区分的判断标准只有一条:这段代码在应用启动、运行或作为产物交付时是否真的被加载。凡是生产运行时需要用到的包(框架、运行时库、数据库驱动、对外导出的类型依赖)放进 dependencies;只在开发、测试、构建、类型检查、lint 阶段用到的包(编译器、打包器、测试框架、@types 私有类型包、格式化工具)放进 devDependencies。判断不了时,先按“生产环境少装一个包会报错吗”这条线做初判,再用下面小节中的命令核对。
把包放进 dependencies 还是 devDependencies,取决于该包是否出现在生产运行时链路上:运行时代码 import 的、被服务端启动脚本加载的、对外发布包的类型声明依赖,通常放 dependencies;构建工具、测试框架、lint 与本地类型注解一般放 devDependencies。操作上先用npm ls和代码里的 import 对照,再在干净环境用npm ci --omit=dev安装并启动验证。边界是 peerDependencies、optionalDependencies 以及对外发布库的类型依赖,需要单独判断,不能一刀切。
先确认当前场景和目标
在动手改 package.json 之前,先确认三件事,否则很容易把能跑的工程改坏。
- 这个包是应用还是被发布出去给别人的库。 应用型项目只要保证生产环境能启动即可;对外发布的库还要考虑使用方安装时是否需要这个包,以及导出的类型声明里是否引用了它,这两种场景结论可能相反。
- 生产环境怎么装依赖。 如果部署脚本或镜像构建用的是
npm ci或npm install不带裁剪参数,那两种依赖都会被装上,区分只影响安装体积和安装时长;如果用了--omit=dev或--production,区分就直接决定服务能不能起来。 - 运行时加载路径。 服务端在启动入口 require/import 的、被依赖注入配置加载的、被同构代码在浏览器端执行的,都算运行时链路;只在
scripts里被build、test、lint调用的,基本属于开发链路。
这三条确认完,再决定是“整体迁移若干包”还是“只补充个别包的归属”,目标不同,处理顺序也不同。

按这个顺序处理
- 先看清现状。 分别导出两份清单,避免凭印象改:
npm pkg get dependencies
npm pkg get devDependencies
npm ls --depth=0
npm ls 展示的是已经安装并实际解析到的依赖树,比 package.json 上写的更能反映当前状态;如果某个包出现在树里但不在两份清单中,说明它是被当作 peer 依赖或间接依赖带进来的,先不要动它。
- 用代码反查真实引用。 在源码目录里搜索包名,确认引用位置。只出现在测试文件、构建配置、类型声明文件里的,通常可以归到开发依赖;出现在启动入口、路由、数据访问层的,保留在 dependencies。
- 核对脚本。 打开 package.json 的
scripts,看build、test、lint用到的命令行工具是否本来就该在开发依赖里;很多误判是因为把打包器、CLI 工具直接npm install装进了 dependencies。 - 执行迁移。 归属明确的包用命令改,不要手改 JSON 后忘记同步 lock 文件:
npm install --save-dev some-build-tool
npm install --save-prod some-runtime-lib
每次改完确认 package.json 与 package-lock.json 都已更新,两者不同步时 CI 上的 npm ci 会直接失败。

- 对拿不准的包做保守处理。 如果一个包只有在特定功能分支才会被加载,建议先留在 dependencies,确认该功能在生产中可达后再定,避免因为裁剪依赖导致线上某个路径才报错、排查成本高。
改完后怎么验证
验证要模拟“只有生产依赖”的环境,而不是在当前 node_modules 上直接跑,因为残留的包会掩盖问题。
- 在干净的副本或临时目录里执行
npm ci --omit=dev,观察安装过程是否报错;较新的 npm 支持--omit=dev,老版本可用--production,需要结合团队实际版本确认。 - 安装完成后跑
npm ls --omit=dev --depth=0,确认被迁走的包已经不在生产依赖列表里,仍在列表里的包都是预期内的运行时依赖。 - 启动应用或执行生产构建产物,逐条走一遍关键链路:启动入口加载、数据库或外部服务连接、涉及被迁移包的那个功能路径。预期信号是启动无模块找不到错误、关键接口能正常响应。
- 单独检查类型检查和构建:
npm run build、npm test应正常完成,说明开发依赖没有被误删。
如果验证只能在本地进行,把“生产环境用 --omit=dev 安装”这条写进部署脚本或 CI 配置,让后续每次发布都自动校验,而不是靠人工记。
容易忽略的边界
- peerDependencies 不受这个划分影响。 插件、UI 组件库、eslint 配置包通常把宿主框架声明为 peer 依赖,交由使用方安装,不要顺手挪进 dependencies,否则容易出现同一宿主被装成多份。
- optionalDependencies 的语义不同。 这类包允许安装失败而不中断整体安装,通常用于平台相关的二进制模块,把它当普通运行时依赖迁移会改掉容错行为。
- 对外发布库的类型包要单独看。 如果导出的类型签名引用了某个
@types/*,使用方做类型检查时可能需要它;这类包是放 dependencies 还是 devDependencies,取决于发布方式和构建产物,需要结合项目实际确认,不能沿用应用项目的习惯。 - monorepo 与私有源。 工作区内互相引用的包用 workspace 协议,不走 registry 语义;私有源的可用性和缓存策略也会影响安装结果,迁移依赖时优先在贴近真实网络条件的环境中验证一次。
- 回退方式。 迁移后如果出现生产环境模块缺失,优先把对应包移回 dependencies 并重新生成 lock 文件,而不是在生产环境手工补装;紧急情况下可临时调整部署命令保留完整安装,但应尽快把依赖归属改正确,不要让完整安装长期作为默认方案。