前几天我们把前端的 10 多个站点和微应用全部合并到了同一个 Monorepo 仓,原以为用上 Turborepo 后构建效率能直接起飞,结果一上线 GitLab CI 直接被打脸。打包构建时间从以前单库的 3 分钟直接飙升到了 22 分钟。看了下构建日志,发现虽然开了 Turborepo 的 Remote Caching,但缓存命中率居然是接近零。

排查之后才发现踩了大坑。GitLab CI 每次跑 Pipeline 都会拉取全新的 runner 环境,如果没有做好 inputs 边界的划分,哪怕你只是改了一个 sub-package 里的 README,Turborepo 也会因为哈希计算不一致把所有站点的构建全部重跑一遍,远程缓存等于形同虚设。

我们原先的 turbo.json 写得极其粗糙,几乎把项目根目录下所有的东西都塞进了输入源哈希计算中:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*", "**/*.ts", "**/*.tsx", "package.json", "tsconfig.json"],
      "outputs": [".next/**", "dist/**"]
    }
  }
}

这里面的致命错误在于,inputs 里的匹配规则写得太宽泛了。比如 **/*.ts 直接穿透了所有的 node_modules 甚至是构建生成的临时缓存文件。加上每次 CI 执行时,系统自动生成的 .env 或者临时配置文件变动,都会导致全局哈希完全失效。

为了彻底解决这个痛点,我们做了一次深度的构建调优。首先,必须严格缩减 inputs 的计算边界,把与构建无关的代码文件排除出去。特别是在单库微服务架构下,我们需要利用 Turborepo 的 inputs 过滤机制,只对特定目录、特定后缀并且确实影响最终 bundle 的文件做 hash 运算。

以下是我们重构后、能在 GitLab CI 稳定实现分布式远程缓存命中率 85% 以上的 turbo.json 配置:

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    "tsconfig.json",
    "package.json"
  ],
  "tasks": {
    "build": {
      "dependsOn": [
        "^build"
      ],
      "inputs": [
        "src/**/*",
        "public/**/*",
        "next.config.js",
        "postcss.config.js",
        "tailwind.config.js",
        "vite.config.ts",
        "!**/*.test.ts",
        "!**/*.spec.ts",
        "!**/__tests__/**/*"
      ],
      "outputs": [
        "dist/**",
        ".next/**",
        "!.next/cache/**"
      ],
      "cache": true
    },
    "lint": {
      "inputs": [
        "src/**/*"
      ],
      "cache": true
    }
  }
}

注意上面配置里的 !**/*.test.ts!**/*.spec.ts。我们排除了所有的测试文件。因为如果改了测试用例,完全没有必要让生产环境的打包缓存失效。

接下来是 GitLab CI Pipeline 的配合。由于我们没有买 Vercel 的官方 Remote Cache 服务,所以我们干脆用 MinIO 在内网搭了一个兼容 S3 协议的私有对象存储服务。然后通过开源的 turbo-rclone 或者直接通过 Turborepo 1.x / 2.x 支持 of 自定义 --api 参数把缓存往私有云端同步。

.gitlab-ci.yml 里,我们是这样注入远程缓存凭证并执行构建的:

stages:
  - build

variables:
  TURBO_API: "https://turbo-cache.internal.ourdomain.com"
  TURBO_TEAM: "frontend-arch"

build_job:
  stage: build
  image: node:20-alpine
  before_script:
    - npm ci --prefer-offline --no-audit
  script:
    - export TURBO_TOKEN="${INTERNAL_TURBO_TOKEN}"
    - npx turbo run build --filter=...[origin/main] --api="${TURBO_API}" --token="${TURBO_TOKEN}" --team="${TURBO_TEAM}"
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm/

在执行命令时,我们用了 --filter=...[origin/main]。这个过滤器的巧妙之处在于,它能自动计算当前分支相较于 origin/main 发生改动的所有 package 及其下游依赖项,避免对完全没变动的 package 执行无意义的构建检查。

这一套组合拳打下来,GitLab CI 上的打包时间直接从 20 多分钟缩短到了平均 3.5 分钟。只有在修改全局公共依赖包时才会触发一次全量的构建。

对于遇到类似构建卡顿的团队,建议先通过 turbo run build --summarize 生成构建分析 JSON,看看究竟是哪个文件的哈希漂移导致了缓存失效。别盲目堆 CPU 配置,找出哈希漏斗才是正解。