1. 写在最前面:为什么你需要关心依赖清理
前端项目跑着跑着,package.json 里的依赖越来越多,这是几乎所有团队都绕不开的坎。我见过太多项目,dependencies 和 devDependencies 里躺着几十个根本没人引用的包,每次 install 都要多等几秒,CI 构建时间被白白拉长,node_modules 体积膨胀到几个 G,还有更隐蔽的问题:某个包其实还在被使用,但因为版本冲突被升级后悄悄换了行为,你排查到最后才发现是依赖树出了问题。
Depcheck 就是用来解决“依赖混乱”这个问题的。它做一件非常朴素的事:扫描你的项目,告诉你哪些依赖被实际使用了,哪些是多余的,哪些被用到了但没写进 package.json。这个工具不是新东西,但绝大多数开发者只是听过名字,真正用熟的人不多。这篇指南我想从实际使用角度,把 depcheck 的原理、用法、坑和团队实践完整过一遍,希望你看完能直接在自己的项目里用起来,并且踩过的坑我尽量帮你提前踩平。
# 安装 depcheck npm install -g depcheck # 或者局部安装 npm install --save-dev depcheck全局安装适合平时随手检查,局部安装适合集成到 CI 和 npm scripts 里,我建议项目内局部安装,原因后面讲 CI 集成时会详细说。
2. 依赖管理失控的典型症状
2.1 你的项目是不是也有这些症状
第一个症状:package.json 里有一堆你完全不认识的包。每次接手老项目,打开 package.json 就像考古,某公司项目的依赖列表里甚至躺着七八个UI组件库,仔细查代码,真正在用的只有两个,剩下的都是早期技术选型遗留物,某程序员在某次迭代里试着引入,后来推翻重写,但package.json没清干净。
第二个症状:删掉某个依赖后,项目跑不起来了。这类问题最折腾人,因为生产者是某个深层依赖悄悄依赖了它,你的代码里没直接引用,但运行时就是需要。这类问题在 monorepo 和大型业务项目里特别常见。
第三个症状:package.json 里的版本和 lock 文件对不上。这类问题在多人协作的项目里经常发生,有人改了 package.json 但没提交 lock 文件,或者在合并分支时冲突解决得不仔细,导致 lock 文件里有残留记录。
这三个症状背后的原因,本质上是 package.json 记录的“意图”和代码实际需要的“现实”之间发生了偏离。depcheck 就是用来检测这种偏离的。
2.2 依赖清理为什么难
你可能会想,删个依赖有什么难的?用的时候删掉不就行了。但现实是,代码里查找依赖是否被使用,远比想象中复杂。
先说 JS 代码。一个包可能只在注释里出现了,也可能作为字符串被动态拼接后传给 import(),也可能是某个宏展开后的依赖,光靠 grep 是排查不干净的。再说样式代码,scss 里可能用了某个包的 mixin,这个包在 JS 代码里完全没有 import,如果你只搜 JS,很容易误判。还有配置文件,比如 PostCSS 配置里用了某个插件,但这个插件在业务代码里无名无姓。
depcheck 的思路是:针对不同文件类型分别做解析,尽可能多地识别出“真正被代码引用的依赖”,然后把它们和 package.json 里的记录做比对。注意,它做的是静态分析,不是运行时分析,所以它给出的结果是“静态代码层面的使用情况”。
depcheck 返回的三类信息: - Unused dependencies: 在 package.json 中声明,但代码中没有检测到使用 - Unused devDependencies: 同样,但属于开发依赖 - Missing dependencies: 代码中使用了,但 package.json 中没有声明理解这三类信息,就是理解 depcheck 的核心。很多人只关注 Unused,其实 Missing 更值得注意,因为它意味着你的依赖声明是不完整的。
3. Depcheck 的核心原理和认识误区
3.1 它是怎么工作的
depcheck 的工作流程大致分四步:
第一步,解析 package.json,得到当前项目声明的所有依赖列表。第二步,根据项目特征识别文件类型,它内置了对 JavaScript、TypeScript、Vue、Sass/Less、PostCSS 等常见文件类型的支持。第三步,针对每种文件类型做解析,提取出文件里引用到的模块名。这一步是核心,比如对于 JS 文件,它用 AST 解析语法树,找出所有 import 语句、require 调用、define 调用等。第四步,把提取到的模块名和 package.json 里的依赖列表做比对,输出的差异就是未使用依赖和缺失依赖。
这个原理决定了它的一个重要特性:depcheck 只能识别静态可解析的引用。如果你的代码里有动态 require ,变量名拼出来的模块路径,depcheck 大概率识别不出来。
// 这种动态 require,depcheck 通常无法识别 const moduleName = 'lodash'; const lib = require(moduleName); // 这种字符串拼接的动态引用也一样 import(`./locales/${lang}.json`)遇到这种情况,你需要用 depcheck 的 ignore 配置,显式告诉它哪些依赖是实际在用的,避免被误报为 Unused。
3.2 常见认识误区
误区一:depcheck 说 unused 就一定能删。不对,depcheck 是静态分析工具,它看不到运行时行为。有些依赖是在配置里被“隐式”使用的,比如 Babel 插件、ESLint 插件、Webpack 插件,它们大多不直接在业务代码里 import,但却是构建链路的关键一环。如果你无脑删,项目构建大概率直接挂掉。
误区二:depcheck 说 missing 就一定要立刻补上。也不一定。它识别出代码里用了某个模块但 package.json 没声明,这个模块可能是某个依赖的“依赖”,也就是你项目里没有直接依赖它,但它被间接依赖了,并且你的代码直接 import 了它。这种行为叫“依赖提升”或“幽灵依赖”,在 pnpm 环境下基本不存在,但在 npm 和 yarn classic 下很常见。补上声明是更健康的做法,但需要在理解依赖关系的前提下操作。
误区三:depcheck 能替代人工 review。不能。它是最佳辅助,不是最终裁判。你在提交清理 PR 之前,一定要至少跑一遍构建、测试、启动项目,甚至点一下关键流程。工具告诉你“可能没用到”,你自己要去确认“确实没用到”。
4. 安装与基础使用全流程
4.1 安装方式怎么选
临时用一次,全局装就可以:
npm install -g depcheck depcheck --help项目长期维护,建议局部安装:
npm install --save-dev depcheck为什么建议局部安装?一是版本可控,团队成员都使用 package.json 里锁定的版本,不会因为全局版本不一致出现结果差异;二是 CI 集成方便,npm install 之后直接用 npx depcheck 就可以执行;三是新人加入项目时 npm install 一次就自动装好了,不需要额外解释全局环境配置。
4.2 第一次运行:无参数模式
npx depcheck默认会扫描当前目录,输出报告。这里有个很重要的参数 --json,把它加上后可以得到结构化输出,方便后续处理:
npx depcheck --json输出结果大概是这样的结构:
{ "dependencies": { "unused": ["axios", "lodash", "some-old-lib"], "missing": ["dayjs"] }, "devDependencies": { "unused": ["eslint-plugin-import", "prettier"], "missing": [] } }第一次扫描完,不要急着删。先看 unused 列表,把每个包都用 grep 再确认一遍,因为 depcheck 有误报的可能,特别是涉及动态引用、配置文件隐式引用、样式语言引用等情况。
4.3 指定文件类型和解析器
有的项目用了比较冷门的语法或文件类型,depcheck 默认解析器覆盖不到,这时候需要显式指定:
npx depcheck --parsers "**/*.ts": "@typescript-eslint/parser" npx depcheck --parsers "**/*.vue": "vue-parser"depcheck 官方内置了几个常用 parser,包括:
- JavaScript: es6 / babel - TypeScript: typescript - Vue: vue - JSX: jsx - Sass/Less: sass / less - PostCSS: postcss如果你的项目用了特殊文件后缀或者自定义 DSL,就需要手动扩展 parser。这个功能使用频率不高,但遇到特殊项目时,它是关键救命技能。
4.4 忽略特定依赖
当你确认某些依赖确实在用,depcheck 却一直误报时,用 ignore 参数:
npx depcheck --ignore "eslint-plugin-*,@types/*"也支持从文件里读取忽略列表:
npx depcheck --ignore-path .depcheckignore.depcheckignore 文件格式类似 .gitignore,一行一个模式:
eslint-plugin-* @types/* babel-plugin-*我个人建议团队统一维护一个 .depcheckignore 文件,因为构建链路的隐式依赖往往比较多,散落在命令行里不如集中在一个文件里好维护。
5. 深度实战:在真实项目中清理依赖
5.1 先备份,再做差异分析
清理依赖是有风险的操作,不能直接在主干上动刀。我的习惯是拉一个分支,先跑一遍 depcheck --json 保存一份基线报告,然后逐个确认要删的包。
npx depcheck --json > depcheck-report-before.json把这份报告提交到分支里留底,还能方便后续 Code Review 对照。
5.2 逐类确认 Unused 依赖
拿到 unused 列表后,我按以下几类分组处理:
第一类,业务代码里完全没有引用,也没有配置文件引用,删掉风险最低。直接用 npm uninstall 删除:
npm uninstall some-old-lib第二类,业务代码没有引用,但配置文件里引用了。比如某个 webpack loader 被配置在 webpack.confg 里,业务代码不直接 import,但构建要用。这种情况不能直接删,要么把对应配置也一并移除,要么加入 .depcheckignore。
第三类,代码里确实没有静态引用,但运行时有隐式使用。这类最阴险。常见场景是:项目里用了某个全局插件,它在运行时通过全局对象暴露接口,你的代码通过 window. 方式访问,导致静态解析识别不到。这种依赖要不要删,得回归业务逻辑判断,如果确实还在用,就加到 ignore 列表里并写注释说明原因。
我给团队的建议是:每个列入 ignore 的依赖,都应该在 .depcheckignore 文件里加一行注释,写明为什么这个依赖被忽略,否则三个月后没人知道这个 ignore 列表为什么存在。
# 运行时通过全局API使用,静态分析识别不到 spa-global-utils # 构建器内部使用,业务代码不直接引用 webpack-dev-middleware5.3 处理 Missing 依赖
Missing 依赖的处理思路不太一样。代码里用了但 package.json 没声明,理论上应该补上声明。但这里有个陷阱:他可能是依赖提升导致的“伪 missing”。
举个我在某跨部门项目里遇到的例子:项目 A 依赖包 B,包 B 又依赖包 C,项目 A 的代码里直接 import 了 C。包 C 没有出现在项目 A 的 package.json 里,但运行的时候,npm 把 C 提升到了顶层 node_modules,代码能跑。depcheck 检测到代码里用了 C,package.json 里没有 C,于是报 Missing。
这种依赖提升的写法,在 npm 和 yarn classic 下能跑通,换到 pnpm 就会直接报错。所以补上这些 Missing 依赖声明,不只是让 depcheck 满意,更是让项目在未来的包管理器迁移中不至于翻车。
npm install dayjs --save npm install webpack-loader-helper --save-dev注意:补 Missing 依赖的时候要用 --save 或 --save-dev,把它正式写进 package.json,而不是装一下不记录。
5.4 清理后的回归验证清单
依赖删完后,至少要跑这几步验证:
# 1. 删除 node_modules 和 lock 文件确保干净安装 rm -rf node_modules package-lock.json npm install # 2. 执行类型检查 npx tsc --noEmit # 3. 执行测试 npm test # 4. 手动构建 npm run build # 5. 启动开发服务器,手动过一遍关键页面 npm run dev这五步一个都不能省。我见过太多人只跑了个 npm run build 就觉得稳了,结果启动 dev 服务器后,某个动态导入的模块直接 500。构建和运行是两回事。
6. 配置工作流:让 depcheck 真正融入日常
6.1 封装成 npm scripts
在 package.json 的 scripts 里加一个专用命令:
{ "scripts": { "depcheck": "depcheck --json > .depcheck/output.json && node scripts/depcheck-check.js" } }那个 node 脚本主要作用是解析 json,按照团队的规则判断有没有需要拦截的错误级别。比如有的团队希望 Missing 依赖直接报错,Unused 依赖只提示不拦截:
// scripts/depcheck-check.js const fs = require('fs'); const report = JSON.parse(fs.readFileSync('.depcheck/output.json', 'utf8')); const unusedCount = report.dependencies.unused.length + report.devDependencies.unused.length; const missingCount = report.dependencies.missing.length + report.devDependencies.missing.length; if (missingCount > 0) { console.error(`发现 ${missingCount} 个缺失依赖,必须补全声明`); process.exit(1); } if (unusedCount > 0) { console.warn(`发现 ${unusedCount} 个未使用依赖,建议及时清理`); }这里要提一下 .gitignore,别把 .depcheck 目录提交进仓库,它只是过程产物。
6.2 接入 CI 做自动检查
CI 是 depcheck 发挥最大价值的地方。本地跑 depcheck 靠自觉,CI 拦截才是制度保障。
我在某中型项目里的做法是:在 CI 流水线里加一个 job,每次 PR 触发时跑一次 depcheck,结果作为检查项。配置大概是这样的:
dependency-check: stage: lint script: - npm install - npm run depcheck only: - merge_requests except: - main这样一个简单的配置,就能拦住大部分“删了代码忘了删依赖”和“加了代码忘了加依赖”的问题。团队里成员每天都能在 PR 反馈里看到依赖健康状态,时间一长,依赖清理就变成了肌肉记忆。
6.3 配合 CI 时要注意的坑
CI 环境跑 depcheck,最大的坑在于 monorepo 和 workspace 项目。默认情况下 depcheck 是按单个 package.json 扫描的,在 monorepo 里需要针对每个子包单独执行。我在某 monorepo 项目里是这么处理的:
npx lerna exec -- depcheck --json或者用 npm workspaces 的项目,可以用 -- 参数指定扫描路径:
npx depcheck packages/package-a --json npx depcheck packages/package-b --json另一个坑是 CI 环境首次 npm install 可能装不齐依赖,因为有些依赖只在某 npm script 里被安装。如果 depcheck 扫描时报解析器找不到,先排查一下依赖是否安装完整。
6.4 搭配其他工具:工具链组合拳
depcheck 只是工具链里的一环,我推荐搭配下面几个工具一起用:
- npm-check: 交互式地更新依赖版本,和 depcheck 互补 - pnpm audit: 检查依赖安全性,depcheck 不管安全 - npm outdated: 查看依赖版本更新情况,depcheck 不管版本 - size-limit: 监控打包体积增量,防止清理完又膨胀回去如果你在维护开源库,还有一件事需要考虑:depcheck 默认会扫描 package.json 里 bin 字段指向的文件和 main 字段指向的入口文件。对于 lib 目录和 example 目录,如果有意不纳入检查,要显式排除。
npx depcheck --ignore-dirs lib,example,dist7. 实际配置中常见坑点排查
7.1 depcheck 误报 Unused 的各类场景
误报是最常见的问题。我总结几个高发场景。
第一个场景:子包间互相引用。monorepo 里子包之间的依赖通过 workspace 协议声明,depcheck 有时候会把同属于 workspace 的其他子包识别为 Unused,这是因为它的解析器没有正确理解 workspace 协议。解决方案是升级到最新版本,并且确认 Node 版本符合要求,部分老版本对 workspace 支持确实有问题。
第二个场景:TypeScript 类型引用。如果你在代码里用了某个类型,但只是 import type 形式引入,depcheck 大概率能识别;但如果你通过三斜线指令引用:
/// <reference types="node" />depcheck 默认可能识别不到,需要显式配置 typescript parser 和自身版本要匹配。
第三个场景:CSS 文件里 @use 和 @import 了 Sass 库。depcheck 自带 Sass parser,但某些新版 Dart Sass 的语法解析可能会失败,需要升级 depcheck 版本。
第四个场景:纯配置文件消费的包。任何写在 .babelrc、.eslintrc、postcss.config.js 里但代码里不 import 的包,都要考虑是否加入 ignore。
7.2 输出结果无法正常解析
如果你用的 depcheck 版本比较新,输出的是纯 JSON,正常 parse 没问题。但如果你在脚本里用 exec 系统命令拿 stdout,要注意 depcheck 可能会把提示信息也写到 stdout 里,纯文本混在 JSON 里导致 parse 失败。稳妥的做法是用 --json 配合 --no-color 参数:
npx depcheck --json --no-color > report.json如果还是解析失败,先手动打开 report.json 看一眼尾部有没有异常输出。我遇到过脚本莫名其妙 parse 不了,最后发现是某个依赖的 warnings 信息混进了 stdout。
7.3 依赖装了但代码里识别不到
有一种常见情况:新装了一个依赖,还没来得及在代码里引用,depcheck 会把它报告为 Unused。这其实是“预期行为”,关键是你要区分“还没开始写业务代码”和“写了但没引用”。
处理思路是:如果团队采用分支开发,一个分支只做依赖清理,那新装依赖后没引用也说得通;如果依赖已经装了三个月、代码里始终没有引用,基本可以确定是多余的。这类判断不能靠工具,要靠你对业务的理解。
7.4 排查表
我把最常见的几个问题整理成一张表,方便你贴到团队文档里。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 依赖被误报为 Unused | 动态 import 或 require | 加入 .depcheckignore 或改用静态引用 |
| 依赖被误报为 Unused | 配置文件隐式引用 | 确认配置文件引用,加入 ignore |
| 依赖被误报为 Unused | workspace 协议未识别 | 升级 depcheck,确认 Node 版本 |
| Missing 依赖频繁出现 | 依赖提升导致幽灵依赖 | 显式补全 package.json 声明 |
| 解析器报错 | 文件后缀不在默认支持范围 | 使用 --parsers 指定 |
| monorepo 扫描结果混乱 | 单 package.json 模式不适配 | 逐包执行项目级扫描 |
8. 高级玩法:自定义解析器和跳过专项检查
8.1 给特殊文件类型写自定义解析器
有些项目可能包含特定后缀的模板文件,比如 .tpl、.hbs,depcheck 没有内置对应解析器。这时候可以写一个小插件来识别模板里的模块引用。
depcheck 支持通过扩展包的方式添加自定义 parser,常见的做法是在 .depcheckrc 文件里配置:
{ "parsers": { "**/*.tpl": "my-custom-template-parser" }, "specials": ["bin", "eslint", "webpack", "babel"] }自定义 parser 需要实现一个接口,接收文件内容,返回提取到的模块引用列表。这里不展开具体 API,因为大多数人的项目用不上。真正要说明白的是:depcheck 的可扩展性意味着只要你有争议,它可以被调整到适配你的项目。
8.2 specials 参数与构建工具链
depcheck 有一个 specials 配置,它能把一些常见工具链的“隐式依赖”识别出来。比如:
npx depcheck --specials=eslint这会让 depcheck 读取 .eslintrc 配置,把其中加载的插件都视为使用中的依赖,即使业务代码里完全没有 import。同理还有 webpack、babel、jest 等 special。如果你在标准项目里直接跑 depcheck,这些 specials 默认是开启的。
但对于高度自定义的构建链,比如某些内部封装的构建脚本,specials 覆盖不到,这时候就把对应依赖写进 .depcheckignore。
8.3 自动化脚本:清理未来新增的废依赖
说一个我最近的实操。我给某项目的 CI 加了一个“依赖健康评分”机制。每当 PR 合并时,跑一遍 depcheck,统计 unused 和 missing 的数量,存到项目的监控数据里。一旦 unneeded 数量超过阈值,就在下一个 PR 的检查项里报警。
这个阈值怎么定?我建议参考项目规模和团队迭代速度。小项目 0 个最好,中型项目允许存在 0~2 个低频误报项(记录在 ignore 文件里),大型项目需要严格卡 0,因为大型项目依赖总量大,任何一个多余的包都会放大构建时间。
// 简单的阈值卡控示例 const MAX_UNUSED_ALLOWED = 2; if (unusedCount > MAX_UNUSED_ALLOWED) { console.error(`未使用依赖数量 ${unusedCount} 超过阈值 ${MAX_UNUSED_ALLOWED}`); process.exit(1); }这样,依赖清理就从“手工定期大扫除”变成了“日常自动保洁”,团队就不用每次大版本发布前花一整天理依赖了。
9. 团队落地经验分享
9.1 新人加入第一个任务
我在带团队时,会让新人在熟悉了项目结构和代码规范之后,用 depcheck 做一次全量扫描,然后以 PR 形式提交清理结果。这么做有四个好处:
一是新人通过清理依赖,能快速了解项目里每一处代码的位置和模块间的关系;二是清理操作本质上是低风险重构,适合新人建立信心;三是依赖清理 PR 涉及面广,新人能因此和资深同学产生大量 Code Review 讨论,快速了解项目的架构坑点;四是每一次依赖清理都会带来实实在在的 CI 速度提升,让新人感受到自己的贡献有直接价值。
9.2 和 lint 流水线配合
很多项目的 lint 脚本只检查代码风格。我建议把它扩一下,把 depcheck 加入“软检查”范围。代码风格错误是硬性的,会影响代码质量观感;依赖缺失则是软性的,影响的是长期可维护性。
一种折中的接入方式是:CI 里 depcheck 结果默认 warning,只有连续多个 PR 里同一个依赖都被识别为 Unused 时才提升为 error。这样不会让团队在刚开始用的时候天天因为误报而抱怨工具不好用,又能慢慢建立依赖健康基线。
9.3 定期依赖治理日
我个人的体会是:depcheck 适合日常跑,但依赖治理还是需要定期专项处理。每个季度抽出半天,按 depcheck 的输出把依赖彻底过一遍,清掉一批废包,更新一批版本,顺手把 Node 版本对齐。这个节奏比天天盯着 depcheck 输出更适合大多数团队。
在我最近一次季度治理中,某项目通过 depcheck 清理了 17 个未使用依赖和 5 个缺失依赖,CI 构建时间从 12 分钟降到了 9 分钟,node_modules 体积从 2.1G 降到了 1.6G。不是说 depcheck 是万能的,但它确实让清理这件事从“靠感觉”变成了“有据可依”。
10. 写在最后:关于依赖治理的一点个人体会
用 depcheck 这几年,我最大的感受是:依赖管理不是技术问题,而是工程习惯问题。工具只能帮你发现“有什么不对”,但真正让项目保持健康的是团队对“依赖是有成本的”这个认知。每多一个依赖,就多一点安装时间、多一点安全风险面、多一点升级成本、多一点排查难度。
depcheck 的价值不是“因为大家都在用”,而是它刚好卡在了一个关键的工程节点上:在依赖失控造成严重问题之前,用最低的成本把问题暴露出来。比它更早介入的是代码审查,比它更晚介入的是每次构建时的性能消耗。它在中间,帮你建立一条“依赖健康基线”。
删依赖的时候记得留分支,跑完 CI 再合入,新装依赖后花十分钟确认一下是否真的用上了,年底大扫除时把 depcheck 当第一个工具跑一遍。
我在实际使用中还有一个很小的习惯:每次跑完 depcheck 清理之后,顺手把 lock 文件里那些古董依赖的间接版本也清理一遍——运行 npm install 的时候你会发现某些包更新到了兼容的新版本,这才是真正的焕新。依赖治理没有终点,但这个工具能让你跑得更稳。