从零搭建高效稳定的CI/CD流水线实战经验分享

公孙红梅 工具 阅读 1,383
赞 19 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

我们团队用 GitHub Actions 跑前端 CI/CD,项目是 Vue 3 + Vite 构建的管理后台,代码量中等(约 12w 行),组件不算特别多但用了不少第三方 UI 库和 mock 工具。CI 流水线跑一次,从 push 到部署完成,平均要 5.8 秒 —— 等个构建结束,我都顺手刷完一条朋友圈了。

从零搭建高效稳定的CI/CD流水线实战经验分享

更难受的是:本地 npm run build 只要 1.6s,CI 却慢三倍多;而且每次改一行 CSS,CI 都要重跑整个流程:install → lint → test → build → deploy,中间还夹着一堆没必要的 cache 读写、重复下载依赖、全量打包……最夸张的一次,一个只改了 README.md 的 PR,CI 跑了 4.3s,我盯着那个绿色勾号,差点点开 GitHub 支持中心问“你们服务器是不是卡了?”

找到瘼颈了!

我先不急着改配置,开了个空仓库,用 act 本地模拟跑了一遍完整流水线(act -j build),加了 timeset -x,发现几个明显拖后腿的环节:

  • npm install 平均耗时 1.9s(CI 每次都清 cache 重装)
  • vitest 运行全部单元测试要 1.3s(但很多 PR 根本没动业务逻辑)
  • vite build 默认开启 sourcemap,生成 .map 文件耗时增加 0.7s
  • deploy 步骤里,用 scp 推静态文件到服务器前,硬编码写了 rsync -av --delete ./dist/ user@host:/var/www/,没做任何增量判断,哪怕只改了一个 JS,也全量覆盖

我还试了 actions/cache@v4,但一开始配错了路径,cache key 写成 ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }},结果发现 lock 文件每次 CI 都被重写(因为 pnpm 自动更新 resolved 字段),导致 cache 命中率几乎为 0 —— 这个坑我踩了两天才意识到。

优化后:流畅多了

试了几种方案,最后这个效果最好,也是最简单、改动最小、收益最大的组合拳:

第一招:精准缓存 node_modules

改用 pnpm workspace + pnpm store 全局缓存 + 锁定 store path。GitHub Actions 官方文档其实早有提示,但没人细看 —— 我把 cache key 换成:

- uses: actions/cache@v4
  with:
    path: ~/.pnpm-store
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}

同时在 workflow 中提前运行 pnpm store path 确保路径一致,并在 install 步骤加 --store-dir ~/.pnpm-store。这一下,install 从 1.9s 直降到 0.3s(对,就是 300ms)。

第二招:按需跑测试

不用全量跑 vitest。Vite 本身支持 vitest related,但我懒得改脚本,直接用 GitHub 提供的 github.event.commits[0].modified 列表(注意:这个字段在 push event 里可用,在 PR event 里不可靠,所以我在 PR 流程里换成了 git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }})来筛文件,然后只跑被修改文件对应的 test 文件:

# 在 workflow 的 test 步骤里加 shell 脚本
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }})
TEST_FILES=""
for file in $CHANGED_FILES; do
  if [[ $file == *"src/"* && $file == *".ts" ]]; then
    test_path=$(echo $file | sed 's/src/test//src//; s/.ts$/.test.ts/')
    if [ -f "$test_path" ]; then
      TEST_FILES="$TEST_FILES $test_path"
    fi
  fi
done

if [ -z "$TEST_FILES" ]; then
  echo "No test files changed, skip tests"
  exit 0
fi

pnpm vitest run $TEST_FILES

实测:90% 的 UI 修改类 PR,只触发 1~2 个 test 文件,测试时间从 1.3s 降到 0.2s

第三招:关掉 sourcemap + 压缩 inline

Vite 默认 dev 模式不开 sourcemap,但 build 会开。我直接在 vite.config.ts 加了:

export default defineConfig({
  build: {
    sourcemap: false,
    rollupOptions: {
      output: {
        inlineDynamicImports: true
      }
    }
  }
})

再配合 build.rollupOptions.output.inlineDynamicImports = true,减少 HTTP 请求,顺便把打包体积压小了 8%,build 时间从 1.4s 降到 0.8s

第四招:deploy 做 diff 推送

不用 scp 全量覆盖,改用 rsync --checksum 增量同步(比 --size-only 更准,避免因时间戳错乱漏传):

rsync -avz --checksum --delete ./dist/ user@server:/var/www/

再加个 --dry-run 日志开关,出问题能立刻看到差在哪。实际部署时间波动变小,平均从 0.6s 降到 0.35s

性能数据对比

我拉了优化前后各 20 次 CI 的耗时数据(排除网络抖动异常值),取中位数:

  • 优化前中位耗时:5.78s
  • 优化后中位耗时:0.82s
  • 整体提速:86%
  • install 环节提速:84%
  • test 环节提速:85%
  • build 环节提速:43%
  • deploy 环节提速:42%

最爽的是:现在改个按钮文字,push 后 1 秒内就看到绿色勾号,连喝口水的时间都不用等。

当然也有遗憾:PR 评论自动触发 CI 时,git diff 有时会漏判(比如只改了 .gitignore 或 .prettierrc),导致 test 跳过。不过这种场景影响极小,我也懒得补全所有 edge case —— 毕竟上线稳定比完美更重要。

以上是我的优化经验,有更好的方案欢迎交流

这些都不是什么高深技术,全是 GitHub Actions 文档里藏着、但没人当回事的细节。我折腾了三天,记了两页笔记,删了四版 workflow 配置,最后发现最有效的永远是那几行最朴素的命令。

如果你也在用 pnpm + Vite,可以抄走上面那段 cache 和 diff test 的配置,亲测有效。如果你们用的是 yarn 或 npm,思路一样,只是 key 和命令微调一下就行。

另外提醒一句:别迷信“一键加速 Action”,很多第三方 action 包裹了一堆无用逻辑,反而更慢。我自己写的这几段 shell 就够用了。

这个技巧的拓展用法还有很多,比如结合 commit message 触发不同构建策略(feat 开头跑测试,docs 开头跳过 build),后续会继续分享这类实战博客。

以上是我踩坑后的总结,希望对你有帮助。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论