先说结论:PR 的 CI 反馈时间从 92 秒降到 12 秒,改动只有三处——缓存 key、一个 postinstall 脚本、任务编排。没有换工具链。
先测量
动手前先看 CI 自带的分步耗时,取一周的中位数:
| 阶段 | 耗时 | 占比 |
|---|---|---|
| 依赖安装 | 41s | 45% |
| typecheck | 17s | 18% |
| 单元测试 | 14s | 15% |
| lint | 12s | 13% |
| 构建 | 8s | 9% |
另外两个数据:缓存命中率 31%;五个 job 全部串行。这两个数字基本解释了全部问题。
依赖安装:41s → 5s
缓存命中率低的原因藏在 key 里——key 混入了 commit SHA,等于每个提交都强制冷缓存。改成 lockfile 的 hash 后,lockfile 不变就一直命中,命中率稳定在 96%。
# 之前:每个提交都 miss
cache:
key: deps-${{ github.sha }}
# 之后:lockfile 不变就一直命中
cache:
key: deps-${{ hashFiles('pnpm-lock.yaml') }}
paths: [.pnpm-store]
其次是删掉一个 postinstall 脚本(生成版本号文件)。它只被构建用到,挪进了构建 job,安装路径上少了一次 node 进程启动和文件写入。
改完后命中缓存的安装约 5 秒,冷缓存 38 秒。
并行与关键路径
lint、typecheck、单元测试互不依赖,改成并行。串行时总耗时是五段之和,并行后关键路径只剩最慢的一段。
构建挪出 PR 流水线,合并到 main 后再跑。PR 需要回答的是「这个提交能不能合」,不是产出制品。
结果
| 指标 | 之前 | 之后 |
|---|---|---|
| PR 反馈时间 | 92s | 12s |
| 缓存命中率 | 31% | 96% |
| 合并后构建 | 含在 PR 内 | 31s |
| 每周 CI 总时长 | 4.2h | 1.1h |
12 秒的构成:安装 4s + 并行 job 里最慢的 8s。冷缓存时 PR 反馈约 48 秒,一个月碰不上几次。
没做的事
- 没换打包器。评估过 esbuild,但构建已经不阻塞反馈,收益不到 3 秒,不值得引入迁移成本。
- 没做 vendor 目录缓存。pnpm store 缓存修好后,这个问题已经不存在了。
先测量再动手的价值在这里:三处改动覆盖了 80% 的耗时,剩下 20% 的候选方案全部收益不足、复杂度有余。