切换语言
切换主题

GitHub Actions 缓存策略:加速 CI/CD 流水线 5 倍

npm install 3 分 15 秒。

这是我去年接手的一个项目的 CI 构建时间。每次推送代码,我都得盯着 GitHub Actions 的日志转圈,等那个绿色的勾勾出现。说实话,那段时间我经常切到其他窗口摸鱼——反正要等。

后来加了缓存,同样的一次构建,40 秒搞定。快了差不多 5 倍。

这不是魔法,就是配置对了 GitHub Actions 的缓存策略。今天这篇文章,我会把踩过的坑、测试过的数据、以及可以直接复制使用的配置模板都整理出来。如果你也在等 CI 构建,这篇可能能帮你省下不少咖啡时间。

一、缓存机制核心概念

先搞清楚缓存是怎么运作的,不然配置的时候容易踩坑。

GitHub Actions 的缓存机制其实挺简单——就三步:查找 → 恢复 → 保存。你定义一个 key,GitHub 会在所有缓存里找有没有匹配的。找到了,直接恢复到你的工作目录;没找到,等任务跑完再存一份新的。

但有几个硬限制你得知道:

限制项数值
单仓库缓存上限10 GB
单个缓存文件上限5 GB(实际超过 1GB 就容易出问题)
缓存保留期限7 天未被访问即删除
全局并发上传限制最多 5 个缓存同时上传

我见过有人踩过 10GB 的坑——项目依赖太多,缓存越攒越大,最后新缓存存不进去,旧的又被清掉了,每次构建都是”冷启动”。

还有一点容易混淆:Cache 和 Artifact 不是一回事。Cache 是给 CI 用的,追求快;Artifact 是给人看的,比如构建产物、测试报告,要长期保存。Cache 有 10GB 限制,Artifact 没有上限(但会占你的仓库存储)。

另外还有 Docker Layer Cache,这是专门给 Docker 构建用的,逻辑跟普通缓存不太一样,后面会单独讲。

二、缓存键设计策略

缓存能不能命中,全看 key 设计得对不对。这是整个缓存策略的核心。

hashFiles() 是什么

GitHub 提供了一个内置函数 hashFiles(),它能算出文件的哈希值。常用在 package-lock.jsonyarn.lock 上——依赖不变,哈希就不变,缓存就能命中。

key: npm-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

这段的意思是:生成一个形如 npm-Linux-a1b2c3d4e5f6... 的键。只要 package-lock.json 没变,这个键就不会变。

restore-keys:备用方案

但依赖总会更新,这时候就需要 restore-keys。它是个”降级匹配”机制:

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

优先匹配完整 key。匹配不到?降级找 npm-Linux- 开头的旧缓存。虽然没有完全命中,但至少 node_modules 里大部分包都有了,只需要增量安装新依赖。

三种键命名模式对比

我测试下来,推荐这三种模式:

简单模式(适合小项目):

key: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}

版本模式(适合多 Node 版本):

key: {{ runner.os }}-node{{ matrix.node-version }}-{{ hashFiles('**/package-lock.json') }}

多路径模式(适合 monorepo):

key: {{ runner.os }}-{{ hashFiles('**/package-lock.json', '**/yarn.lock') }}

如何判断缓存是否命中

actions/cache 会输出一个 cache-hit 变量:

- uses: actions/cache@v4
  id: cache-npm
  with:
    path: ~/.npm
    key: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}

- name: Check cache hit
  run: echo "Cache hit - {{ steps.cache-npm.outputs.cache-hit }}"

true 表示精确命中,false 表示部分命中或完全 miss。你可以根据这个变量决定要不要跑 npm ci

- name: Install dependencies
  if: steps.cache-npm.outputs.cache-hit != 'true'
  run: npm ci

三、实战配置示例

理论讲完了,直接上代码。以下配置我都实测过,可以直接复制使用。

npm 缓存(推荐用 setup-node)

其实 setup-node 已经内置了缓存功能,比手动用 actions/cache 更简洁:

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # 或 'yarn'、'pnpm'

一行搞定。但如果你想缓存其他目录(比如 node_modules),还是得用 actions/cache

- uses: actions/cache@v4
  with:
    path: node_modules
    key: {{ runner.os }}-nm-{{ hashFiles('**/package-lock.json') }}
    restore-keys: {{ runner.os }}-nm-

我的建议:优先用 setup-node 的内置缓存,除非你有特殊需求。

yarn 和 pnpm

yarn 的缓存目录跟 npm 不一样:

- uses: actions/cache@v4
  with:
    path: |
      ~/.yarn/cache
      ~/.yarn/install-state.gz
    key: yarn-{{ runner.os }}-{{ hashFiles('**/yarn.lock') }}

pnpm 更特殊,它用的是全局 store:

- uses: pnpm/action-setup@v4
  with:
    version: 9

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

Python/pip 缓存

Python 项目的缓存路径:

- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-{{ runner.os }}-{{ hashFiles('**/requirements.txt') }}
    restore-keys: pip-{{ runner.os }}-

Docker Layer Cache

Docker 构建最耗时。好消息是 BuildKit 支持 GitHub Actions 的缓存后端:

- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    cache-from: type=gha
    cache-to: type=gha,mode=max

type=gha 表示用 GitHub Actions 的缓存服务存 Docker 层。实测下来,一个 5 分钟的镜像构建能降到 1 分钟左右。

Go 模块缓存

- uses: actions/cache@v4
  with:
    path: |
      ~/go/pkg/mod
      ~/.cache/go-build
    key: go-{{ runner.os }}-{{ hashFiles('**/go.sum') }}

Rust Cargo 缓存

- uses: actions/cache@v4
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target
    key: cargo-{{ runner.os }}-{{ hashFiles('**/Cargo.lock') }}

Rust 编译慢,缓存能省大量时间。但要小心 target 目录会越来越大——建议定期清理。

四、性能优化与最佳实践

我整理了一些实测数据和踩过的坑,希望能帮你少走弯路。

性能基准数据

根据 RunsOn 的测试报告(2026 年 1 月更新),合理配置缓存后:

操作无缓存有缓存提升
npm install3 分钟40 秒约 5 倍
yarn install2 分钟 30 秒35 秒约 4 倍
Docker build5 分钟1 分钟约 5 倍
pip install45 秒8 秒约 5 倍

缓存命中率在 70-90% 之间,取决于你的键策略设计得够不够好。

常见陷阱

不要直接缓存 node_modules

我一开始就是这么干的,结果踩了大坑。

# 不要这样写
path: node_modules

node_modules 是平台特定的——Linux 上装的包,Windows 跑起来可能有问题。正确做法是缓存全局缓存目录(~/.npm),让 npm ci 自己去组装。

跨 OS 缓存要用 GNU tar + zstd

默认的 tar 在 macOS 和 Windows 上格式不一样,会导致缓存恢复失败。加这个配置:

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}
    enableCrossOsArchive: true

缓存污染问题

有时候缓存里存了有问题的依赖,导致构建一直失败。解决办法:

  1. 手动删除缓存:进入 GitHub 仓库的 Actions → Caches 页面,点击删除
  2. 强制更新 key:给 key 加个前缀或时间戳,让它重新生成
key: npm-v2-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

最佳实践清单

最后总结几个要点,配置前对照检查:

  1. 优先使用官方 action 的内置缓存(setup-node、setup-python)
  2. key 要包含 hashFiles,不然依赖更新了缓存还在用旧版本
  3. restore-keys 写上,降级匹配能保命
  4. 不要缓存 node_modules,缓存全局目录
  5. 定期清理过期缓存,避免超出 10GB 限制

五、常见问题解答

Q1: 为什么我的缓存命中率不高?

最常见的原因是 key 变得太频繁。比如你在 key 里加了时间戳或者分支名,每次 push 都会生成新 key。解决方案:只用 runner.oshashFiles,去掉不必要的变量。

另一个原因是 hashFiles 匹配了不该匹配的文件。比如你写了 hashFiles('**/*.json'),结果配置文件改一下,缓存就失效了。改成只匹配 package-lock.jsonyarn.lock

Q2: 缓存空间超限怎么办?

10GB 看起来挺大,但 monorepo 或 Docker 缓存很容易爆。解决方案:

  1. 定期清理:GitHub Actions → Caches,手动删除旧的
  2. 分开缓存:不同依赖用不同 key,避免一个缓存存所有东西
  3. 用 self-hosted runners:它们没有 10GB 限制

Q3: self-hosted runners 需要特殊配置吗?

不需要特殊配置,缓存机制一样用。但 self-hosted runners 有个优势:缓存是本地存的,不存在网络传输延迟,恢复速度更快。缺点是缓存不会自动清理,得自己写脚本定时清。

Q4: 如何强制更新缓存?

改 key。加个前缀版本号:

key: npm-v3-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

或者直接删掉旧缓存,让系统重新生成。

总结

说了这么多,其实就是一句话:用好缓存,CI 能快 5 倍。

我给你算笔账——假设每次构建省 2 分钟,每天跑 10 次,一个月就是 600 分钟,差不多 10 小时。这时间够写好几篇文章了。

如果你刚上手 GitHub Actions,建议先从 setup-node 的内置缓存开始,一行配置就够用。等遇到瓶颈了,再回来研究更复杂的键策略和 Docker Layer Cache。

对了,这篇文章是 GitHub Actions 实战指南系列的第 3 篇。之前写过 CI 流水线搭建和部署策略,感兴趣可以翻翻历史文章。

下次推送代码的时候,记得看看你的构建时间。能不能从 3 分钟降到 40 秒,试试就知道。

配置 GitHub Actions 缓存加速 CI/CD

通过配置 GitHub Actions 缓存,将 npm install 构建时间从 3 分钟降至 40 秒

⏱️ 预计耗时: 10 分钟

  1. 1

    步骤1: 选择缓存方案

    根据项目包管理器选择缓存方案:

    • npm 项目:优先使用 setup-node 内置缓存
    • yarn/pnpm 项目:配置缓存路径
    • Docker 构建:使用 BuildKit 的 gha 后端
  2. 2

    步骤2: 设计缓存键

    使用 hashFiles() 基于锁文件生成稳定键:

    • 基础模式:{{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}
    • 添加 restore-keys 作为备用匹配
    • 避免在 key 中加入时间戳或分支名
  3. 3

    步骤3: 添加缓存配置

    在 workflow 文件中添加缓存步骤:

    • npm:使用 actions/setup-node@v4,设置 cache: 'npm'
    • 自定义路径:使用 actions/cache@v4
    • Docker:设置 cache-from 和 cache-to
  4. 4

    步骤4: 验证缓存效果

    检查缓存是否命中:

    • 查看 cache-hit 输出变量(true 为精确命中)
    • 对比构建时间(应减少 4-5 倍)
    • 检查 Actions → Caches 页面确认缓存已存储
  5. 5

    步骤5: 定期维护缓存

    避免缓存问题:

    • 监控缓存空间使用(上限 10GB)
    • 定期清理旧缓存
    • 遇到污染时更新 key 前缀强制重建

常见问题

缓存命中率只有 30% 怎么回事?
通常是 key 设计问题。检查是否在 key 中加入了频繁变化的变量(如时间戳、分支名),改为只用 runner.os 和 hashFiles。另外确认 hashFiles 的路径是否精确匹配锁文件,避免使用通配符匹配过多文件。
缓存了 10GB 以上会怎样?
GitHub 会自动清理最旧的缓存来腾出空间。建议:

• 分开缓存不同类型的依赖(npm、Docker、pip 各用一个 key)
• 定期在 Actions → Caches 页面手动删除无用缓存
• monorepo 项目考虑分仓库或使用 self-hosted runners
不同分支能共享缓存吗?
默认情况下,缓存仅在当前分支和默认分支(main/master)之间共享。如果你想跨分支共享,需要在 key 中去掉分支名,只使用基于文件的 hash。另外 restore-keys 可以帮助匹配其他分支的缓存。
self-hosted runner 的缓存有什么不同?
缓存机制相同,但有两点差异:优势是缓存存在本地,恢复更快(无网络延迟);劣势是没有 10GB 限制但也不会自动清理,需要自己写脚本定时清理旧缓存。
缓存恢复失败构建会中断吗?
不会。缓存是可选优化,恢复失败不影响构建。GitHub Actions 会继续执行后续步骤,只是这次构建会重新下载依赖。你可以在日志中看到 'Cache not found for key: xxx' 提示,然后会自动保存新缓存供下次使用。
如何判断缓存是否需要更新?
三种情况需要更新缓存:

• 依赖版本变化:hashFiles 自动处理,无需手动干预
• 缓存污染:构建突然失败,需要清除旧缓存
• 配置变更:如 Node 版本升级,需要在 key 中加入版本号

大多数情况下,正确配置后无需手动管理。

9 分钟阅读 · 发布于: 2026年4月7日 · 修改于: 2026年4月8日

评论

使用 GitHub 账号登录后即可评论

相关文章