CI集成实战:从零搭建高效自动化流水线
优化前:卡得不行
上周我们项目 CI 流水线跑一次前端构建,动不动就 8 分钟起步,有时候甚至飙到 12 分钟。每次提 PR 都得盯着那个转圈圈的 CI 状态,心里直打鼓——是不是又超时了?是不是缓存没生效?本地跑 build 只要 40 秒,CI 上却慢成狗,这差距也太大了。
最离谱的是,测试阶段还经常因为超时被 kill 掉,团队里好几个同事都开始绕过 CI 直接 merge(别学!)。说实话,这已经不是“有点慢”,而是严重影响开发节奏了。
找到瓶颈了!
我决定花一天时间死磕这个问题。首先打开 GitHub Actions 的日志,逐行看每个 step 花了多少时间。结果发现:
npm install平均耗时 3 分钟- Webpack 构建 4 分钟
- 单元测试反而只用了 30 秒
好家伙,光安装依赖和构建就占了 90% 的时间。再细看 npm install,发现它每次都在重新下载所有包,哪怕 package-lock.json 完全没变。查了一下 GitHub Actions 的文档,原来默认是不持久化 node_modules 的——除非你手动配缓存。
另外,Webpack 构建过程中,SourceMap 生成占了将近 1 分钟,而我们的 CI 根本不需要上传 SourceMap 到生产环境,纯属浪费。
优化一:给依赖加缓存,亲测有效
先解决 npm install 的问题。GitHub Actions 提供了 actions/cache,但很多人配置不对,导致缓存命中率极低。我试了三种方案:
- 用
node_modules作为缓存路径 —— 失败,因为 node_modules 太大,上传下载反而更慢 - 只缓存
~/.npm—— 好一点,但跨 Node 版本时容易出问题 - 缓存
node_modules的 hash,基于package-lock.json内容生成 key —— 最终采用这个
核心思路是:只要 lock 文件不变,就直接恢复整个 node_modules,跳过 install。
折腾了半天,终于搞定了。关键配置如下:
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci
这里注意我踩过好几次坑:一定要用 npm ci 而不是 npm install,否则即使缓存命中,npm 还会去检查 registry,白白浪费时间。另外,restore-keys 是为了兼容 lock 文件微小变动时还能回退到最近的缓存,避免完全 miss。
优化二:关掉 CI 上的 SourceMap
Webpack 那边更简单。我们用的是 Create React App,虽然不能直接改 webpack.config.js,但可以通过环境变量控制。
在 CI 脚本里加一行:
GENERATE_SOURCEMAP=false npm run build
就这么一行,构建时间直接少了 50 秒。如果你是自定义 Webpack 配置,那就更直接了:
// webpack.prod.js
module.exports = {
mode: 'production',
devtool: process.env.CI ? false : 'source-map',
// ...
}
别小看这个改动,尤其在大型项目里,SourceMap 生成可能是 CPU 密集型操作,CI 机器通常资源有限,关掉它立竿见影。
优化三:并行跑测试和构建?别乱来!
有人建议把测试和构建并行跑,听起来很美,但我试了发现反而更慢。因为我们的 CI runner 是 2 核机器,Webpack 和 Jest 同时跑会抢 CPU,互相拖后腿。最后还是串行更稳。
不过,如果你们用的是高配 runner(比如 4 核以上),可以试试用 concurrently 或者 GitHub Actions 的 matrix job 拆分任务。但我们这种小团队,省点钱用低配机器,还是老老实实串行吧。
性能数据对比
上优化前后数据,说话才有底气:
| 阶段 | 优化前(平均) | 优化后(平均) | 下降比例 |
|---|---|---|---|
| npm install | 180s | 8s(缓存命中) | 95.5% |
| Webpack 构建 | 240s | 190s | 20.8% |
| 总耗时 | 480s(8分钟) | 210s(3分30秒) | 56.2% |
现在 CI 平均 3 分半跑完,PR 状态秒变绿,团队幸福感直线上升。最关键的是,失败率从 15% 降到几乎为 0——再也不用因为“CI 超时”重跑五次了。
还有一些小技巧
除了上面三个大头,我还顺手做了几件小事:
- 升级 Node.js 版本到 18.x(Actions 里指定
node-version: '18'),新版 V8 对 npm 和构建工具本身就有提速 - 删掉项目里几个没人维护的 devDependencies,减少 install 时间
- 把 ESLint 和 TypeCheck 放到 pre-commit hook,不在 CI 上跑(除非是 main 分支)
这些加起来又能省个 10-20 秒,积少成多嘛。
最后说两句
这次优化最大的体会是:不要假设 CI 和本地一样快。CI 环境资源有限、网络不稳定、缓存机制复杂,很多本地无感的操作在 CI 上就是性能杀手。
另外,别盲目追求“全自动优化”。像缓存策略,必须根据你的依赖变更频率调整。我们团队一周 lock 文件变两三次,所以缓存命中率能到 80%;如果你每天都在升级依赖,那可能收益就没那么大。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流——比如你们是怎么处理 monorepo 的 CI 缓存的?我一直觉得那玩意儿更难搞。

暂无评论