2026.10.06
lint 从 374 秒到 33 秒,以及从 0 到 56 个单测
一个项目最容易腐烂的地方不是功能,是反馈回路。这篇记录怎么把每次改动的等待从六分钟压到半分钟,以及为什么补齐测试比加速 lint 更重要。
给一个维护中的项目做工程化,最先要回答的问题是:开发者改一行代码,多久能知道自己有没有搞砸?
我接手时的答案是:六分钟以上。
npm run lint 单次要跑 374 秒。这个数字的后果不是"慢",而是没人会跑它——六分钟的等待会让人养成"先提交,让 CI 去发现问题"的习惯,而 CI 反馈的延迟更长。
lint 为什么慢#
排查下来是两件事叠在一起:
- 扫描范围失控。构建产物、本机临时目录、被 gitignore 掉的中间文件全在被扫范围内。这些文件不是源码,但 linter 不知道。
- 类型感知规则太重。带类型信息的规则(需要构建 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,改动时你只知道"代码看起来对",不知道"行为还对"。
第一批测试我优先挑了三类:
- 纯函数(最容易测,收益最直接)——比如前面提过的 Markdown 安全切块器。它一旦切错位置,表现是"渲染看起来怪怪的",而不是报错。这种必须有测试。
- 边界条件密集的逻辑——令牌刷新的并发去重、限流分类。这些是靠肉眼看不出问题的。
- 曾经出过 bug 的地方——每个修过的 bug 都补一个测试,保证它不会回来。
第 3 条尤其值。回归测试的价值等于"这个 bug 曾经真实发生过"。
顺带清掉的死代码#
抽查依赖时发现两个大包根本没有被引用:recharts 和 milkdown。它们进了 package.json 但没有任何 import 路径指向它们。
移除之后构建产物和安装时间都小了一圈。这类问题的成因通常是"某个版本试过、后来不用了、忘了删"——定期用工具扫一遍未使用的依赖是值得的。
结果#
| 指标 | 改造前 | 改造后 |
|---|---|---|
| lint 单次耗时 | 374 s | 33 s(−91.2%) |
| 单元测试数 | 0 | 56 |
| lint 基线 | 本地 48 error(假红) | 0 error / 38 warning |
| 未使用的大依赖 | recharts、milkdown | 已移除 |
一点想法#
工程化这个方向很容易被当成"杂活",但它改变的是一件很实在的事:反馈回路的长度。
374 秒的 lint 和 33 秒的 lint,差别不在那 341 秒,而在于前者根本不会被跑。一套没人执行的规范,等于没有规范。而 56 个测试的意义也不在覆盖率数字,而在于改动时你敢不敢动——不敢重构的代码库,才是真正的技术债。