严格区分 dependencies 和 devDependencies,这件事听起来像每个前端/Node 开发者入职第一天就该会的常识,但现实是我接手过的项目里,十个有八个把 qrcode、moment、axios 这种运行时库放在 devDependencies,又把 eslint、jest、typescript 塞进生产依赖。还有更常见的:项目跑得挺好,但没人敢动 package.json,因为一 npm install 就冒出一堆“幽灵包”,npm ls 列出来全都带 extraneous 标记。
这个标题的核心其实就是两件事:先想清楚包该放哪一类,再回头把不该存在的包清干净。前者影响部署环境、镜像大小、CI 缓存命中率;后者影响开发者日常安装速度、依赖树健康度、以及最容易被忽视的供应链攻击面。这篇文章就当是一次现场排障记录,我会把判断依据、实操命令、以及我踩过的坑全部拆开讲,适合所有用 npm、yarn、pnpm 维护项目的同学参考。
1. 先弄明白:为什么“归类”错了会非常麻烦
1.1 一个错误归类导致的生产事故复盘
两年前我维护过一个 Node 服务,package.json 里 dependencies 足足有 180 个包,其中一半以上都是构建期工具。有一次部署流水线优化,运维小哥把 Docker 镜像改成多阶段构建,生产阶段只执行 npm install --production,结果服务启动直接报 Cannot find module 'ts-node/register'。
原因很好笑:项目里有人为了本地调试方便,把 ts-node 写进了 dependencies,生产环境安装时确实装上了,但真正的业务代码用了 dist 下的编译产物,根本不需要它。而 dotenv 这种运行时必须的库,反而因为当时图省事被放进了 devDependencies,生产环境一装,连配置文件都读不进来。
这事背后是两种风险:
- 放错到 devDependencies 的运行时包,一旦部署环境使用
--production或NODE_ENV=production安装,应用启动就会原地崩掉。 - 放错到 dependencies 的工具包,会让生产镜像白白多出几十上百 MB,更重要的是这些包平时只在开发机上有漏洞通告,一旦被带上生产环境,等于主动扩大了攻击面。
所以“归类”不是 package.json 里的排版问题,它直接决定部署产物里到底有什么。
1.2 两类依赖的本质区别不是“常用”和“不常用”
很多人对 dependencies 和 devDependencies 的理解停留在“前者是项目运行需要的,后者是开发时候用的”。这句话基本对,但缺少一个最重要的判断标尺:这段代码是否会被打包进你的线上运行产物?
只要回答“是”,就该进 dependencies;只要回答“否”,那就进 devDependencies。
从 npm 的视角看,dependencies 里的包会被当成“应用运行时不可分割的一部分”,npm install --production 会照单全收;devDependencies 则只服务于开发工作流——编译、测试、代码检查、本地 mock,生产环境安装时会被完全忽略。
再讲得直白一点,Node 服务端项目里:
- 被
require()或import()的业务代码直接依赖的库,express、koa、mongoose、mysql2、dotenv、ioredis这些,放 dependencies。 - 负责把 TypeScript 编译成 JS 的
typescript、ts-node,做单元测试的jest、vitest,代码规范工具eslint、prettier,这些只在开发期干活,放 devDependencies。
前端项目因为最终产物是静态文件,很多人会想“反正都 webpack 打包进去了,dist 里又没有 node_modules”,于是随便放。但要注意两点:
- 如果你的前端项目需要服务端渲染(SSR)或者接入
next start/nuxt start这类生产启动命令,服务端运行时依赖依然是生产环境需要安装的。 - CI 阶段如果用
npm ci --omit=dev来装依赖再执行构建,而你的eslint、postcss被错放成了生产依赖且业务构建真的用到了,虽然不会报错,但每次构建都必须承担这部分安装开销。
1.3 语义化版本与版本锁定的隐性影响
另一个容易忽略的点是 package-lock.json 会把两类依赖分开记录,但这并不意味着分类不会影响锁文件。
如果你把 webpack 放进 dependencies,它连同它的几十个传递依赖都会进入生产依赖树;如果你把它放回 devDependencies,锁文件里生产区瞬间瘦身。对于部署时要执行 npm ci 的团队,生产依赖越少,下载时间越短,CI/CD 跑得越快,漏洞扫描的范围也越小。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 正确归类:一整套可以直接抄的判断流程
2.1 五分钟判断法:问四个问题
面对每一个包,你不需要背规则,按顺序问四个问题就行。
- 问题一:项目在线上运行(或用户使用构建产物)时,这个包是否会被加载?
- 是 → dependencies;
- 不确定 → 继续问;
- 问题二:去掉这个包,本地开发和构建还能正常跑吗?
- 不能 → devDependencies;
- 能 → 这包大概率连 devDependencies 都不该留;
- 问题三:这个包是“脚手架”还是“运行时工具”?
- 脚手架 / 编译 / 校验 / 测试 → devDependencies;
- 提供数据读写、网络、加解密、业务 SDK → dependencies;
- 问题四:项目是否存在服务端启动、定时任务、脚本消费场景?
