前几天我们把前端的 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 配置,找出哈希漏斗才是正解。