小Cの已经记不起来的博客

在 CI 流水线中实现前端构建产物缓存以缩短流水线时间

上礼拜给项目改了两行样式,提了个 MR,结果流水线跑了十四分钟才出结果。点开日志一看,光 npm install 就啃了六分钟,npm run build 又啃了五分钟,剩下的时间全耗在上传产物和拉镜像上。每次都全量重装全量构建,代码可能就改了一行 css,属实有点离谱。然后就去搜了一圈缓存方案,结果发现大部分教程都只讲一半,教你缓存 node_modules,很少有人把构建产物缓存这块讲明白,干脆自己踩一遍坑记下来。

先搞清楚时间花在哪了

把流水线日志摊开看,时间主要耗在两块:依赖安装和构建。依赖安装其实好办,npm 自己有下载缓存,把 ~/.npm 目录存下来,第二次安装基本是从缓存里直接解压,速度飞快。真正的大头是构建,而这部分恰恰最没人讲。

顺便说一句,不太建议直接缓存 node_modules 目录。这玩意儿动辄几百兆,存取本身就慢,而且 lockfile 一变整份缓存就作废了。不如只缓存 npm 的下载缓存,让每次 install 照常走解压流程,慢是慢一点,但稳定得多。

依赖安装的缓存

我这里用的是 GitHub Actions,写法很简单,核心就是 actions/cache:

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

hashFiles 会根据 lockfile 内容算个 hash,lockfile 没变就命中缓存,变了就作废。restore-keys 是个兜底,精确的 key 没命中的时候,就找前缀最接近的那份凑合用,虽然没有完全命中,但好歹能少下一半包。

构建产物才是大头

思路其实很朴素:lockfile 没变、源码也没变的话,构建出来的 dist 必然一模一样,直接把上次的产物拿来用,构建这一步整个跳过。

- uses: actions/cache@v4
  id: dist-cache
  with:
    path: dist
    key: dist-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**', 'public/**', '*.config.*', 'tsconfig.json') }}

- name: 构建
  if: steps.dist-cache.outputs.cache-hit != 'true'
  run: npm run build

重点是 if: steps.dist-cache.outputs.cache-hit != 'true' 这行,命中缓存就直接跳过构建。不出问题的话就没有问题了,改注释、改文档这类提交,整个流水线从十四分钟掉到四分钟出头,效果立竿见影。

踩过的几个坑

第一,缓存 key 里必须把 lockfile 的 hash 包含进去。我一开始图省事只用了源码的 hash,结果有次升级了 vite,源码一行没动,直接命中了旧产物,页面一打开就白屏,排查了半天才发现是缓存的问题。依赖变了产物可能就不一样了,这个 hash 不能省。

第二,key 的覆盖范围一开始没写全。hashFiles('src/**') 只管 src 目录,像 public/、vite.config.ts、tsconfig.json 这些动了是不会触发缓存失效的,改了环境变量相关的配置照样拿旧产物在跑。所以最后把配置文件也拼进了 key,就是上面那段最终版的写法。

第三,GitHub Actions 的缓存单个仓库上限 10GB,而且 7 天没被访问就会被清掉。别把什么大文件都往里塞,不然真正该缓存的反而被挤掉了。另外有时候确实需要强制重新构建,可以留个手动开关,把 if 条件前面再拼一个 workflow_dispatch 的 input 判断,留个后门心里踏实。

GitLab CI 是另一套写法

GitLab 用自带的 cache 关键字,不用装 action:

build:
  stage: build
  cache:
    - key:
        files:
          - package-lock.json
      paths:
        - .npm/
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
  artifacts:
    paths:
      - dist/

注意两点:一是 GitLab 的缓存在流水线的不同 job 之间会自动传递,构建和部署如果是分开的 job,dist 用 artifacts 传就行,这个不需要缓存,本来流水线跑完就删了;二是 npm ci 本身就会删掉 node_modules 重装,所以别指望在 GitLab 里缓存 node_modules 来省时间,老老实实缓存 .npm 下载缓存即可。

最后

目前这套方案跑了小一个月,没再出过幺蛾子。缓存这东西说难不难,难就难在 key 的设计,什么时候该失效、什么时候该兜底、哪些文件变了产物才会变,这几个问题想清楚了,剩下就没什么问题了。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放