2026.10.06

lint 从 374 秒到 33 秒,以及从 0 到 56 个单测

一个项目最容易腐烂的地方不是功能,是反馈回路。这篇记录怎么把每次改动的等待从六分钟压到半分钟,以及为什么补齐测试比加速 lint 更重要。

给一个维护中的项目做工程化,最先要回答的问题是:开发者改一行代码,多久能知道自己有没有搞砸?

我接手时的答案是:六分钟以上。

npm run lint 单次要跑 374 秒。这个数字的后果不是"慢",而是没人会跑它——六分钟的等待会让人养成"先提交,让 CI 去发现问题"的习惯,而 CI 反馈的延迟更长。

lint 为什么慢#

排查下来是两件事叠在一起:

  1. 扫描范围失控。构建产物、本机临时目录、被 gitignore 掉的中间文件全在被扫范围内。这些文件不是源码,但 linter 不知道。
  2. 类型感知规则太重。带类型信息的规则(需要构建 TypeScript Program 的那些)在每个文件上都重复构建类型图。

第 1 条好办,把忽略清单写全:

// eslint.config.mjs
export default [
  {
    ignores: [
      ".next/**",
      "out/**",
      "node_modules/**",
      // 本机临时目录 —— 这几个目录里也有 .ts 文件,
      // 之前 48 个 error 全部来自这里,CI 上反而是绿的
      "tmp/**",
      "**/chrome-profile-*/**",
      "scripts/**",
    ],
  },
  // ...
];

这里有个特别值得记住的坑:本机临时目录里的文件会被当作源码扫描。当时本地跑出 48 个 error,CI 却全绿——因为那些目录被 gitignore 了,CI 上根本不存在。本地红灯、CI 绿灯,是最消耗信任的一种状态:你会开始怀疑 lint 本身。

第 2 条的做法是把类型感知规则只在 CI 上开,本地开发用轻量规则。本地要的是"快",CI 要的是"全"。

结果:374 秒 → 33 秒(−91.2%),基线回到 0 error / 38 warning。

比加速更重要的事:0 → 56 个单测#

lint 快了之后,我做了一件优先级更高的事:补测试。从 0 到 56 个单元测试。

为什么说它比加速 lint 更重要?因为这两个工具防的是不同类别的错误:

lint 能防lint 防不住
未使用变量是
类型不匹配是
空指针 / 边界条件是
业务逻辑错是
并发时序错是

lint 是风格与类型的护栏,测试是行为的护栏。一个项目只有 lint,改动时你只知道"代码看起来对",不知道"行为还对"。

第一批测试我优先挑了三类:

  1. 纯函数(最容易测,收益最直接)——比如前面提过的 Markdown 安全切块器。它一旦切错位置,表现是"渲染看起来怪怪的",而不是报错。这种必须有测试。
  2. 边界条件密集的逻辑——令牌刷新的并发去重、限流分类。这些是靠肉眼看不出问题的。
  3. 曾经出过 bug 的地方——每个修过的 bug 都补一个测试,保证它不会回来。

第 3 条尤其值。回归测试的价值等于"这个 bug 曾经真实发生过"。

顺带清掉的死代码#

抽查依赖时发现两个大包根本没有被引用:recharts 和 milkdown。它们进了 package.json 但没有任何 import 路径指向它们。

移除之后构建产物和安装时间都小了一圈。这类问题的成因通常是"某个版本试过、后来不用了、忘了删"——定期用工具扫一遍未使用的依赖是值得的。

结果#

指标改造前改造后
lint 单次耗时374 s33 s(−91.2%)
单元测试数056
lint 基线本地 48 error(假红)0 error / 38 warning
未使用的大依赖recharts、milkdown已移除

一点想法#

工程化这个方向很容易被当成"杂活",但它改变的是一件很实在的事:反馈回路的长度。

374 秒的 lint 和 33 秒的 lint,差别不在那 341 秒,而在于前者根本不会被跑。一套没人执行的规范,等于没有规范。而 56 个测试的意义也不在覆盖率数字,而在于改动时你敢不敢动——不敢重构的代码库,才是真正的技术债。