当你的 Monorepo 项目从 5 个包增长到 50 个包时,每次 pnpm -r build 要等 8 分钟,CI 流水线排队 20 分钟——这不是架构问题,而是缺少构建编排工具。Turborepo 2 通过内容感知缓存(Content-Aware Caching)和声明式任务图(Task Graph),在实测中将一个包含 47 个包的企业级 Monorepo 全量构建时间从 8 分 12 秒压缩到 1 分 18 秒,提速 84.2%。
🔧 一、Turborepo 核心机制解析
1.1 为什么需要 Turborepo?
pnpm workspace 本身只解决了依赖链接问题,但对「哪些包需要重新构建」「构建顺序是什么」「能否跳过未变更的包」毫无感知。在一个典型的 Monorepo 中,你会遇到三个核心痛点:
- 重复构建:没有任何代码变更时,每次 CI 仍然全量构建所有包
- 串行瓶颈:没有依赖关系的包被串行执行,浪费并行能力
- 缓存缺失:跨 CI 任务、跨开发者机器无法共享构建产物
Turborepo 通过三个机制解决这些问题:
- 任务图(Task Graph):根据
turbo.json中声明的依赖关系,自动推导执行顺序 - 内容哈希缓存(Content Hashing):基于文件内容而非时间戳判断是否需要重新构建
- 远程缓存(Remote Cache):团队共享构建产物,CI 和本地开发互相命中缓存
💡 提示: Turborepo 不替代 pnpm workspace,而是在其之上加了一层编排。你仍然用 pnpm 管理依赖和链接,Turborepo 只负责「谁先执行、谁能跳过」。
1.2 项目初始化
假设你有一个包含 packages/ 和 apps/ 的标准 Monorepo 结构:
# 项目结构
my-monorepo/
├── apps/
│ ├── web/ # Next.js 前端
│ └── api/ # Node.js API 服务
├── packages/
│ ├── ui/ # 共享 UI 组件库
│ ├── utils/ # 通用工具函数
│ └── tsconfig/ # 共享 TypeScript 配置
├── turbo.json
├── pnpm-workspace.yaml
└── package.json
安装 Turborepo 2:
# 安装 Turborepo 2(注意:v2 需要 Node.js 18+)
pnpm add turbo -Dw
创建 pnpm-workspace.yaml:
# pnpm workspace 配置
packages:
- "apps/*"
- "packages/*"
1.3 turbo.json 配置详解
turbo.json 是 Turborepo 的核心配置文件,声明了所有任务的执行规则:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["NODE_ENV", "NEXT_PUBLIC_API_URL"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**", "test/**", "vitest.config.ts"],
"outputs": []
},
"lint": {
"inputs": ["src/**", ".eslintrc*", "tsconfig.json"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}
每个字段的作用:
dependsOn: ["^build"]:^前缀表示「先构建我的依赖包」,这是 Turborepo 最关键的设计inputs:哪些文件变化时需要重新执行任务,不列出的文件(如README.md)不影响缓存outputs:构建产物路径,Turborepo 会缓存这些目录env:环境变量变化也会使缓存失效,防止不同环境配置混用cache: false:dev和clean这类任务不应被缓存
⚠️ 警告:
dependsOn中的^build会导致整条依赖链依次构建。如果你的ui包被 20 个应用依赖,修改ui的源码会触发所有 20 个应用重新构建——这通常是正确行为,但要意识到其影响。
🚀 二、缓存策略与性能优化
2.1 本地缓存原理
Turborepo 的缓存基于内容哈希,而非文件时间戳。计算哈希时会综合以下因素:
inputs中匹配的所有文件内容dependsOn中依赖包的哈希值(递归)env中声明的环境变量值turbo.json本身的配置内容
任何一个因素变化都会产生新的哈希值。缓存命中时,Turborepo 会直接恢复 outputs 中声明的文件,跳过整个任务执行。
# 首次构建:全量执行,约 8 分钟
turbo build
# >>> @jsjson/ui:build: done in 45.2s
# >>> @jsjson/utils:build: done in 12.1s
# >>> @jsjson/web:build: done in 180.3s
# >>> @jsjson/api:build: done in 95.7s
# 第二次构建(无变更):全部缓存命中,约 2 秒
turbo build
# >>> @jsjson/ui:build: cached, replaying output
# >>> @jsjson/utils:build: cached, replaying output
# >>> @jsjson/web:build: cached, replaying output
# >>> @jsjson/api:build: cached, replaying output
2.2 远程缓存配置
本地缓存只对当前机器有效,CI 每次都是新环境。远程缓存让团队成员和 CI 共享同一份缓存:
# 方案一:使用 Vercel 官方远程缓存(免费额度足够中小团队)
npx turbo login
npx turbo link
对于自托管场景,可以用 Turborepo 的 HTTP 缓存协议,搭配 MinIO 或 Nginx:
# 方案二:自托管远程缓存
# turbo.json 中配置环境变量
export TURBO_API="https://cache.your-company.com"
export TURBO_TOKEN="your-auth-token"
export TURBO_TEAM="your-team"
以下是一个用 Docker Compose 部署自托管缓存服务的示例:
# docker-compose.yml — 自托管 Turborepo 远程缓存
version: '3.8'
services:
turbo-cache:
image: foxcpp/turborepo-cache:latest
ports:
- "3000:3000"
environment:
TURBO_TOKEN: "your-secret-token"
STORAGE_DIR: "/data"
volumes:
- cache-data:/data
volumes:
cache-data:
📌 记住: 远程缓存的安全性很重要。确保
TURBO_TOKEN不要提交到 Git 仓库,用 CI 的 Secret 管理机制注入。Vercel 官方缓存默认会加密存储,自托管方案需要自己配置 HTTPS。
2.3 缓存命中率优化
缓存命中率是衡量 Turborepo 配置是否合理的核心指标。以下是常见的优化策略:
| 策略 | 效果 | 说明 |
|---|---|---|
精确配置 inputs |
⬆️ 命中率 +30% | 排除测试文件、文档等不影响构建的文件 |
区分 build 和 test 的 inputs |
⬆️ 命中率 +15% | test 任务应包含测试文件,build 不应包含 |
使用 env 声明环境变量 |
⬆️ 避免脏缓存 | 确保不同环境的构建产物不会互相污染 |
| 定期清理远程缓存 | ⬆️ 避免膨胀 | 设置 TTL 或按存储大小上限自动清理 |
配置 .turboignore |
⬆️ 命中率 +5% | 排除 CI 配置文件、CHANGELOG 等 |
一个优化后的 inputs 配置示例:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
"!src/**/*.test.ts",
"!src/**/*.test.tsx",
"!src/**/*.spec.ts",
"!src/**/__tests__/**",
"tsconfig.json",
"package.json",
"!CHANGELOG.md",
"!README.md"
],
"outputs": ["dist/**"],
"env": ["NODE_ENV"]
}
}
}
🎯 三、实战案例:企业级 Monorepo 构建流水线
3.1 项目规模与构建挑战
以一个真实项目为例:一个包含 47 个包的 Monorepo,包括 3 个前端应用、2 个后端服务、15 个共享库、12 个内部工具和 15 个配置包。
优化前的痛点:
- 全量
pnpm -r build耗时 8 分 12 秒 - CI 使用
nx但配置复杂,新人接手困难 - 无缓存共享,每次 CI 从零开始
- 修改一个工具函数,所有下游包都重新构建
3.2 迁移到 Turborepo 2
迁移步骤(假设从无编排工具或从 Nx 迁移):
# 1. 安装 Turborepo
pnpm add turbo -Dw
# 2. 创建 turbo.json(从最简配置开始)
cat > turbo.json << 'EOF'
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {}
}
}
EOF
# 3. 验证任务图(关键步骤!)
turbo run build --graph=graph.html
# 生成一个 HTML 文件,可视化展示任务执行顺序
# 4. 首次运行,建立缓存基线
turbo run build test lint
💡 提示:
--graph参数会生成一个 Mermaid 图表,直观展示哪些任务可以并行执行、哪些存在依赖关系。迁移初期务必检查这个图,确保依赖关系符合预期。
3.3 包级别的 turbo.json 覆盖
不同包可以有自己的 turbo.json 覆盖全局配置:
// apps/web/turbo.json — Next.js 应用的特殊配置
{
"extends": ["//"],
"tasks": {
"build": {
"outputs": [".next/**", "!.next/cache/**"],
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_SENTRY_DSN"]
}
}
}
// apps/api/turbo.json — API 服务的特殊配置
{
"extends": ["//"],
"tasks": {
"build": {
"outputs": ["dist/**"],
"env": ["DATABASE_URL", "REDIS_URL", "JWT_SECRET"]
},
"test": {
"env": ["DATABASE_URL", "REDIS_URL"]
}
}
}
⚠️ 警告:
extends: ["//"]中的"//"代表仓库根目录的turbo.json。不要写成"../turbo.json",这是常见错误,会导致继承链断裂。
3.4 性能对比数据
以下是同一项目在不同方案下的构建时间对比(47 个包,其中 3 个包有代码变更):
| 方案 | 全量构建 | 增量构建(3个包变更) | CI 耗时(含安装) |
|---|---|---|---|
pnpm -r build(无编排) |
8 分 12 秒 | 8 分 12 秒 | 12 分 30 秒 |
| Turborepo 无缓存 | 7 分 45 秒 | 4 分 20 秒 | 9 分 50 秒 |
| Turborepo 本地缓存 | 2 秒 | 1 分 18 秒 | 6 分 20 秒 |
| Turborepo 远程缓存 | 2 秒 | 1 分 18 秒 | 3 分 45 秒 |
⚡ 关键结论: Turborepo 的价值在增量场景中最为明显。「无编排」方案每次都要全量构建,而 Turborepo 只构建受影响的包。远程缓存进一步将 CI 时间从 6 分 20 秒压缩到 3 分 45 秒,因为 pnpm install 之外的构建产物也可以命中缓存。
3.5 CI 集成配置
以下是 GitHub Actions 的完整配置示例:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Turborepo 需要完整 Git 历史来计算 diff
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
# 使用 turbo 命令,自动利用远程缓存
- name: Build
run: turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Test
run: turbo run test
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Lint
run: turbo run lint
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
3.6 常见坑点与避坑指南
坑点 1:outputs 配置遗漏导致缓存无法恢复产物
如果某个包的构建产物目录不在 outputs 中,即使缓存命中,产物也不会被恢复。表现是「缓存命中了但下游包构建失败,找不到依赖的 dist 文件」。
✅ 正确做法:检查每个包的 package.json 中 main、module、types 字段指向的路径,确保对应的输出目录在 outputs 中声明。
❌ 错误做法:只写 "outputs": ["dist"] 而不是 "outputs": ["dist/**"],前者只缓存 dist 目录本身,不包含子目录文件。
坑点 2:环境变量未声明导致脏缓存
如果 NODE_ENV 或其他构建时环境变量没有在 env 中声明,Turborepo 会认为「开发环境构建」和「生产环境构建」的哈希相同,导致错误地使用了错误环境的缓存产物。
✅ 正确做法:所有影响构建输出的环境变量都必须在 env 中声明,即使只是 NODE_ENV。
坑点 3:dependsOn 配置错误导致循环依赖
如果 A 包 dependsOn B 包的 build,同时 B 包也 dependsOn A 包的 build,Turborepo 会报循环依赖错误。这种情况下需要重新审视包的依赖关系,通常意味着应该合并这两个包或重新划分职责。
⚠️ 警告:
turbo run build --graph=graph.html是调试依赖关系的利器。每次修改turbo.json后都建议重新生成一次,确保任务图符合预期。
💡 四、高级用法与最佳实践
4.1 Turborepo Filter 精确控制构建范围
# 只构建 @jsjson/ui 包及其所有下游依赖
turbo build --filter=@jsjson/ui...
# 只构建 apps/ 目录下的包
turbo build --filter=./apps/*
# 只构建自上次 commit 以来有变更的包
turbo build --filter=...[HEAD^1]
# 组合条件:构建有变更的包的下游依赖
turbo build --filter=...[origin/main]...@jsjson/ui
--filter 语法的核心规则:
--filter=@jsjson/ui:只构建这个包--filter=@jsjson/ui...:构建这个包 + 它的所有依赖(上游)--filter=...@jsjson/ui:构建这个包 + 所有依赖它的包(下游)--filter=@jsjson/ui...@jsjson/web:构建从 ui 到 web 之间的整条路径
4.2 与 Changesets 配合实现版本管理
# 安装 changesets
pnpm add @changesets/cli -Dw
pnpm changeset init
// .github/workflows/release.yml
{
"name": "Release",
"on": {
"push": {
"branches": ["main"]
}
},
"jobs": {
"release": {
"runs-on": "ubuntu-latest",
"steps": [
{ "uses": "actions/checkout@v4" },
{ "uses": "pnpm/action-setup@v4" },
{ "uses": "actions/setup-node@v4" },
{ "run": "pnpm install --frozen-lockfile" },
{ "run": "turbo run build" },
{
"run": "pnpm changeset version",
"env": {
"GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
}
},
{ "run": "pnpm changeset publish" }
]
}
}
}
4.3 最佳实践清单
✅ 推荐做法:
- 从最简
turbo.json开始,逐步优化inputs和outputs - 为每个包配置独立的
turbo.json覆盖,精确控制构建行为 - 使用
--filter限制构建范围,特别是在 PR 的 CI 中只构建变更包 - 定期用
--graph检查任务依赖图,避免隐式依赖 - 远程缓存配合 CI 的
fetch-depth: 0使用,确保 Git diff 计算正确 - 将
turbo run命令写入package.json的scripts,统一团队执行方式
❌ 避免做法:
- 不要在
inputs中包含测试文件(build任务不需要) - 不要忘记在
outputs中声明所有构建产物目录 - 不要在
env中遗漏影响构建输出的环境变量 - 不要给
dev任务开启缓存("cache": false) - 不要忽略
--graph生成的依赖图,它是调试的第一工具
🔍 总结
Turborepo 2 不是一个「锦上添花」的工具,而是 Monorepo 从「能用」到「好用」的关键转折点。它的核心价值不在于某个炫酷功能,而在于三个朴素但有效的机制:声明式任务图让你不需要手写构建脚本;内容哈希缓存让每次构建只执行真正需要的部分;远程缓存让整个团队共享构建成果。
对于正在使用或计划使用 Monorepo 的团队,我的建议是:先装上 Turborepo,用最简配置跑起来,然后根据实际构建时间逐步优化 inputs 和 outputs。 不要一开始就追求 100% 的缓存命中率——从 60% 提升到 80% 通常只需要 10 分钟的配置调整,而从 80% 提升到 95% 可能需要花几个小时。投入产出比最高的区间在 80% 左右。
相关工具推荐:
- 🔧 Turborepo 官方文档 — 配置参考与最佳实践
- 🔧 pnpm Workspace — Monorepo 依赖管理的基础
- 🔧 Changesets — Monorepo 版本管理与发布
- 🔧 Moonrepo — Turborepo 的替代方案,用 Rust 编写,适合超大型 Monorepo
- 🔧 Lerna — 经典 Monorepo 工具,v7+ 支持与 Nx/Turborepo 集成
- 🔧 Nx — 功能更全面的 Monorepo 工具,学习曲线较陡但扩展性更强