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