CI/CD集成实战:从零搭建高效自动化部署流程
优化前:卡得不行
上个月我们团队的前端 CI/CD 流水线跑一次构建要 5 分多钟,每次提 PR 都得等半天才能看到检查结果。最离谱的是,有时候改了个文案,整个 pipeline 还是把所有依赖重新装一遍、所有测试全跑一遍,连 E2E 都不放过。本地开发还好,但一到合并请求,CI 就像老牛拉破车,动不动就超时失败。
更烦的是,缓存经常失效。明明只改了组件样式,却因为 package-lock.json 的微小变动,导致 node_modules 全部重装。我试过手动加缓存 key,但效果不稳定,有时候缓存命中率不到 30%。那段时间,每天光等 CI 就浪费至少半小时,心态都快崩了。
找到瓶颈了!
实在忍不了,我决定深挖一下。先在 GitHub Actions 的 workflow 日志里逐行看耗时,发现几个明显问题:
npm install平均耗时 2 分 10 秒- 单元测试跑 1 分 40 秒,但其实 80% 的测试文件根本没被改动影响
- 构建产物上传到 Artifacts 又花了 30 多秒,而且每次都传全部文件
我用 act 在本地模拟运行 workflow,配合 time 命令打点,确认了这些确实是主要耗时点。特别是依赖安装阶段,网络波动大时甚至能卡到 3 分钟以上。看来得从这三个地方下手。
依赖安装:缓存策略大改造
之前用的是 GitHub Actions 官方推荐的 actions/cache,但 key 写得太粗糙,只用了 package-lock.json 的 hash。问题在于,只要 lock 文件有变动(哪怕只是版本号微调),缓存就失效。后来我改成组合 key,把 node 版本、操作系统、以及 lock 文件内容都考虑进去,同时用 restore-keys 做 fallback。
但真正提升大的是换包管理器。我们项目其实早就该切 pnpm 了,但一直拖着。pnpm 的硬链接 + 符号链接机制,安装速度比 npm 快不少,而且磁盘占用少。实测下来,pnpm install 平均只要 45 秒,还自带高效的缓存策略。
以下是优化后的 workflow 片段:
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
这里注意我踩过好几次坑:一开始没用 pnpm store path 动态获取路径,直接写死 ~/.pnpm-store,结果在不同 runner 上路径不一致,缓存根本没生效。后来查文档才发现得用命令动态取。
测试:只跑受影响的文件
单元测试这块,我试了两种方案。第一种是用 Jest 的 --changedSince,配合 git diff 自动检测变更文件。但问题在于,如果改了工具函数,可能影响多个测试,这种依赖关系很难自动追踪。
最后选了更简单的办法:在 workflow 里加个判断,如果 commit message 包含 [skip ci-tests] 就跳过测试(当然只对特定分支开放)。不过这治标不治本。
真正有效的还是用 turbo run。它内置了任务依赖图和增量执行,能自动分析哪些测试需要重跑。配置起来也不难,先在 package.json 里定义任务:
{
"scripts": {
"test": "jest",
"build": "vite build"
},
"turbo": {
"pipeline": {
"test": {
"dependsOn": ["^build"],
"outputs": []
},
"build": {
"outputs": [".next/**", "dist/**"]
}
}
}
}
然后在 CI 里替换原来的测试命令:
# 优化前
npm test
# 优化后
npx turbo run test --filter=./src
实测下来,如果只改了一个组件,测试时间从 100 秒降到 15 秒左右。当然,第一次全量跑还是慢,但日常开发中大部分都是小改动,收益很明显。
构建产物:只传变化的部分
之前我们用 actions/upload-artifact 一股脑传整个 dist 目录,其实 90% 的文件都没变。后来改用 changesets 的思路——只对比差异再上传。不过 changesets 主要是发版用的,不太适合 CI。
折腾了半天发现,其实 GitHub Actions 本身支持基于文件 hash 的增量上传,但官方 action 不提供。最后我用了一个社区方案:先生成文件清单,再和上次的对比,只传新增或修改的。
核心代码就这几行:
# 生成当前文件 hash 清单
find dist -type f -exec md5sum {} ; | sort > current.manifest
# 下载上次的清单(如果存在)
if [ -f previous.manifest ]; then
# 找出差异文件
comm -23 <(cut -d' ' -f2 current.manifest) <(cut -d' ' -f2 previous.manifest) > changed_files.txt
else
# 第一次全传
find dist -type f > changed_files.txt
fi
# 只上传 changed_files.txt 里的文件
tar -czf dist-changed.tar.gz -T changed_files.txt
不过这个方案有点重,后来简化了:直接用 rsync 同步到临时目录再上传,省去了手动 diff 的步骤。虽然还是传整个目录,但因为缓存和压缩,实际传输体积小了很多。最终这一步从 30 秒压到 8 秒,够用了。
性能数据对比
把上面几招全加上之后,CI 时间大幅下降。以下是连续 5 次 PR 构建的平均数据:
- 依赖安装:2 分 10 秒 → 45 秒(↓60%)
- 单元测试:1 分 40 秒 → 22 秒(↓78%,小改动场景)
- 产物上传:32 秒 → 9 秒(↓72%)
- 总耗时:5 分 15 秒 → 1 分 20 秒(↓75%)
最爽的是,现在改个按钮颜色,CI 80 秒内就能跑完,基本不用等。而且缓存命中率稳定在 90% 以上,再也不用担心网络抖动导致安装失败。
当然,也不是完美无缺。比如 E2E 测试还没优化,因为涉及真实浏览器启动,暂时没找到好办法。不过日常开发中 E2E 只在主干跑,影响不大。
结尾唠叨两句
这次优化最大的体会是:CI 性能问题往往不是单一环节导致的,得系统性地看整个流水线。别一上来就猛搞并行,先把最耗时的几个点揪出来,针对性解决。另外,工具选型很重要,pnpm + turbo 这套组合拳比死磕 npm 脚本高效多了。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如你们是怎么处理 E2E 测试提速的?我还在找方案……

暂无评论