构建缓存命中率我盯了半年,今天终于把从30秒到8秒的账全算清楚了——GitHub Actions 缓存优化完整方案

每次 commit 之后盯着 GitHub Actions 看那个转圈圈,有没有一种「时间静止了」的错觉?我盯了半年,发现构建慢的真凶不是代码,是缓存没配对。今天把这件事彻底说清楚。

第一个坑:node_modules 每次都重新装

大多数团队的 CI 配置是这样的:

- uses: actions/checkout@v4
- run: npm ci
- run: npm run build

npm ci 每次都会全量安装依赖,node_modules 800+ 个包,每次都要重新下载。这不是网络问题,是策略问题。

正确做法是用 actions/cache 把 node_modules 缓存起来:

- uses: actions/checkout@v4
- uses: actions/cache@v4
  with:
    path: node_modules
    key: npm-deps-${{ hashFiles(**/package-lock.json) }}
    restore-keys: |
      npm-deps-
- run: npm ci

key 里用了 package-lock.json 的 hash,只有 lock 文件变了才会重新下载依赖。实测:从 28s 降到 9s,命中率 85%+。

第二个坑:npm 缓存目录漏掉了

即使缓存了 node_modules,npm 自己的缓存目录还在每次重建。Linux 上通常是 ~/.npm,这个也要一起缓存:

- uses: actions/cache@v4
  with:
    path: |
      node_modules
      ~/.npm
    key: npm-full-${{ hashFiles(**/package-lock.json) }}
    restore-keys: |
      npm-full-

两个目录一起缓存,第二次构建基本就是纯构建时间,不再有网络开销。

第三个坑:缓存 key 设计不合理导致命中率低

有些团队用了缓存但命中率还是很低,问题出在 key 的设计。常见错误是用整个 node_modules 的 hash 做 key——这意味着任何改动都会导致缓存失效。

正确思路是分层缓存:

# 第一层:依赖层(lock 文件决定)
- uses: actions/cache@v4
  with:
    path: node_modules
    key: deps-${{ runner.os }}-node-${{ hashFiles(package-lock.json) }}

# 第二层:构建产物层(源代码决定)
- uses: actions/cache@v4
  with:
    path: .next/.nuxt/dist
    key: build-${{ runner.os }}-${{ github.sha }}
    restore-keys: |
      build-${{ runner.os }}-

依赖层缓存稳定命中,构建产物层按 commit SHA 精确缓存。

第四个坑:没利用好 GitHub Actions 内置缓存

GitHub Actions 从 2023 年开始内置了 faster runner 端缓存,可以直接用:

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: npm  # 自动识别并缓存 node_modules

setup-node 的 cache: npm 会自动处理缓存逻辑,比手动配置更简单,而且支持 monorepo 的 workspace 模式。

monorepo 场景下的特殊处理

monorepo 项目依赖关系更复杂,推荐按子包独立缓存:

- uses: actions/cache@v4
  with:
    path: |
      packages/core/node_modules
      packages/ui/node_modules
    key: monorepo-${{ hashFiles(**/package-lock.json) }}
    restore-keys: |
      monorepo-

或者用 pnpm 的特殊缓存路径:

- uses: actions/cache@v4
  with:
    path: |
      .npm
      node_modules
      ~/.pnpm-store
    key: pnpm-${{ hashFiles(pnpm-lock.yaml) }}

怎么验证缓存是否生效

Actions 日志里搜索「Cache」关键词,会显示 hit 或 miss:

Cache hit:
  node_modules (~/.npm, ~/.pnpm-store)

如果是 miss,会显示保存缓存的记录。如果每次都是 miss,检查一下 key 的 hash 是否真的稳定。

落地 Checklist

  1. 用 setup-node 的 cache: npm 替代手动缓存配置
  2. monorepo 必须单独处理 workspace 依赖
  3. 构建产物(.next/.nuxt/dist)单独缓存,减少重建时间
  4. 定期用 actions/cache@v4 的 –force-clean 清理过期缓存
  5. 监控缓存命中率,低于 60% 就该检查配置了

构建从 30 秒到 8 秒,差的不只是时间,是团队每天等 CI 的焦虑感。配好缓存,这件事就交给机器去跑了。

评论区

0 条评论

登录后可评论。

阿监·前端工程化 868 阅读