Monorepo 构建缓存命中率优化:Turborepo 配置的常见陷阱

📅 2026/7/9 9:25:16
Monorepo 构建缓存命中率优化:Turborepo 配置的常见陷阱
Monorepo 构建缓存命中率优化Turborepo 配置的常见陷阱一、代码改了 1 行整个 Monorepo 重新构建Monorepo 最大的效率武器是增量构建——只重新构建有变更的包及其依赖方。但如果配置不当一次 lint 修复可能导致 30 个包全部重新编译。核心原因依赖图没写对或者输入文件范围设得太大。Turborepo 的turbo.json决定了每个 task 的缓存键。它通过两个字段计算是否命中缓存inputs哪些文件的变更会触发重新构建dependsOn依赖的上下游有哪些 task如果inputs写成[src/**]任何源码变更都会触发。但如果你改用[src/**/*.ts]且只改了.test.ts就不触发——因为有依赖过滤。flowchart TD A[git push 触发 CI] -- B[Turborepo 启动] B -- C{计算全局哈希} C -- D[遍历所有受影响包] D -- E{包级别哈希比较} E --|哈希匹配| F[从远程缓存恢复] E --|哈希不匹配| G[重新构建] F -- H[跳过构建] G -- I[执行 task] I -- J[构建产物写入缓存] H -- K{还有任务?} J -- K K --|是| D K --|否| L[构建完成]二、缓存命中率低下的五个元凶元凶一inputs范围过大// 差任何文件变更都触发 { inputs: [$TURBO_DEFAULT$] } // 好精确指定输入 { inputs: [src/**/*.ts, tsconfig.json] }$TURBO_DEFAULT$包含.gitignore追踪的所有文件。这意味着改了README.md也会导致构建缓存失效。元凶二未声明dependsOnTask B 的结果依赖 Task A 的产物但没有声明。Turborepo 不知道这个依赖关系B 用了 A 的过时产物。元凶三环境变量泄漏任何环境变量的变化都会打破缓存。特别是 CI 中GITHUB_RUN_ID、TIMESTAMP这类每次运行都不同的变量。必须用env字段显式声明需要哪些环境变量或者用globalEnv排除不稳定的变量。元凶四远程缓存未配置本地缓存只在同一个文件系统上有效。CI 的每个 Job 跑在不同的 Runner 上如果没有远程缓存每次都是全量构建。元凶五globalDependencies滥用globalDependencies里放了package.json的 root——意味着任何一个包的依赖变更都触发全局重建。三、生产级 Turborepo 配置{ $schema: https://turbo.build/schema.json, // globalEnv: 只声明需要参与哈希的全局环境变量 // 没有列在这里的变量变更不会使缓存失效 // CI 中的 GITHUB_RUN_ID 等动态变量绝不能放在这里 globalEnv: [NODE_ENV, NEXT_PUBLIC_API_URL], // globalDependencies: 真正的全局依赖文件 // 只有这些文件的变更才会全局重建 globalDependencies: [ .github/workflows/ci.yml, scripts/build-setup.sh ], tasks: { // 构建任务只依赖上游包的构建产物 build: { // dependsOn: 声明依赖关系 // ^build 表示依赖所有上游包的 build 任务 dependsOn: [^build], // inputs: 构建相关的输入文件 // 只包含影响构建结果的文件排除测试文件和文档 inputs: [ src/**/*.ts, src/**/*.tsx, tsconfig.json, next.config.js, $TURBO_DEFAULT$ ], // outputs: 声明产物目录用于缓存和依赖消费 // 下游包的 dependsOn 会引用这些产物 outputs: [dist/**, .next/**, !.next/cache/**], cache: true }, // 类型检查独立于构建任务的缓存策略 // 独立任务的价值在于——可以单独重跑而不影响构建 typecheck: { dependsOn: [^build], inputs: [ src/**/*.ts, src/**/*.tsx, tsconfig.json ], outputs: [], cache: true }, // 测试任务对依赖要求严格 test: { // dependsOn 可以同时依赖 build 和 typecheck dependsOn: [build, typecheck], inputs: [ src/**/*.ts, src/**/*.tsx, src/**/*.test.ts, src/**/*.test.tsx, jest.config.js, tsconfig.json ], outputs: [coverage/**], cache: true }, // Lint 任务最快的缓存路径 lint: { // lint 不依赖任何上游独立执行 dependsOn: [], inputs: [ src/**/*.{ts,tsx,js,jsx}, .eslintrc.js, .eslintignore ], outputs: [], cache: true }, // dev: 开发模式禁用缓存 dev: { cache: false, persistent: true } } }关键配置解析dependsOn: [^build]中的^表示上游依赖的包。Turborepo 读取package.json中的dependencies字段自动推断依赖图。^build告诉它本包的 build 依赖所有上游包的 build 先完成。outputs: [!.next/cache/**]中的!表示排除。.next/cache是 Next.js 的构建缓存目录包含时间戳等不稳定内容不应当被 Turborepo 缓存。四、缓存命中率的衡量与优化如何衡量命中率# Turborepo 提供了详细的缓存日志 turbo run build --summarize # 生成的 .turbo/runs/*.json 中包含详细的缓存命中统计关键指标Full Turbo 命中包本身无变更 上游无变更从缓存完整恢复局部重建上游有变更但本包无变更只需重新链接全量构建本包有变更必须重建理想命中率日常开发 85%CI 环境 60%。优化策略拆分大型包一个包太大时任何局部变更都导致本包重建。拆小后可以减少影响面。产物而非源码传递下游包引用的应该是上游包的构建产物而非源码。这是dependsOn链的核心价值。定期清理远程缓存Vercel Remote Cache 的免费额度 2GB/月超限后走 fallback 全量构建。五、总结Turborepo 的缓存看似开箱即用实则 90% 的中大型 Monorepo 都没配对。核心就三件事inputs精确化、dependsOn图正确、globalEnv最小化。这三件事做到位缓存命中率从 30% 提升到 80% 是常规操作。