🏎️ Turborepo:Monorepo 构建编排与缓存
Turborepo 是 Vercel 做的 monorepo 构建系统。核心价值两件事:按依赖图编排任务顺序 + 基于内容哈希的构建缓存(含远程缓存)。它自己不执行构建,只负责「算该跑什么、按什么顺序跑、能不能跳过」。
🎯 解决什么问题
Monorepo(一个仓库装多个包)常见的痛点:
| 痛点 | 传统做法 | Turborepo 做法 |
|---|---|---|
| 改了底层包,所有上层包都得重跑 | 只能全量 pnpm -r build | 依赖图计算,只跑受影响的 |
| CI 每次全量构建,慢 | 加机器/加时间 | 远程缓存,命中就跳过 |
| 不确定任务顺序对不对,靠人排 | 手工维护顺序 | dependsOn 声明,拓扑排序自动算 |
| 本地结果和 CI 不一致 | 各自跑 | 同一套缓存哈希逻辑 |
📦 快速上手
1. 前提:必须是 workspace
Turborepo 依赖包管理器做 workspace 管理,不能用 npm install 逐目录装。
# pnpm(推荐,Turborepo 默认生态)
pnpm init
pnpm add -D turbo
# 或 npm / yarn / bun,原理相同pnpm-workspace.yaml(Turborepo 只读这个文件拿包列表):
packages:
- "apps/*"
- "packages/*"2. 加 scripts
package.json:
{
"name": "my-monorepo",
"private": true,
"packageManager": "pnpm@9.0.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"check": "turbo run lint test"
},
"devDependencies": {
"turbo": "^2.3.0"
}
}3. 写 turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}4. 跑
pnpm build # 全部包的 build
turbo run build --filter=web # 只跑 web 包
turbo run test # 全部包的 test
turbo run build --dry=json # 看看会跑什么但不真跑📖 turbo.json 字段详解
⚠️ v1 用
pipeline,v2 改成tasks。网上大量老文章写的还是pipeline,已废弃。
tasks — 任务定义
每个 key 是一个任务名,value 是配置。
dependsOn — 依赖声明
| 写法 | 含义 |
|---|---|
"^build" | 依赖所有上游包的 build(^ 表示跨包) |
"build" | 依赖当前包的 build(无 ^,指自己) |
["^build", "build"] | 两者都要 |
"lint" | 依赖当前包的 lint |
这是声明式的——只说「依赖谁」,不写「按什么顺序」,拓扑排序自动算。
{
"tasks": {
"build": { "dependsOn": ["^build"] },
"test": { "dependsOn": ["^build", "build"] },
"dev": { "dependsOn": ["^build"] }
}
}含义:web 依赖 ui,ui 依赖 utils,那么 turbo run build 会自动按 utils → ui → web 顺序跑,且只跑受影响的。
outputs — 缓存产物
这是最容易漏配的一项。 不声明 outputs,Turborepo 会跑任务但不存产物,下游拿不到文件。
{
"tasks": {
"build": {
"outputs": ["dist/**", "build/**", "!.next/cache/**"]
}
}
}! 前缀是排除。.next/cache/** 体积大且不影响产物命中,通常排除掉。
各框架常见产物:
| 框架 | outputs |
|---|---|
| Vite | dist/** |
| Next.js | .next/**, !.next/cache/** |
| webpack | dist/** |
| tsc | lib/** 或 dist/**/*.tsbuildinfo |
| Rust | target/** |
inputs — 缓存哈希的输入范围
默认是 git 追踪的所有文件。需要收窄或放宽时用:
{
"tasks": {
"test": {
"inputs": ["src/**", "tests/**", "*.config.ts"]
}
}
}常用于「测试结果不依赖某些文件」的场景,加快缓存复用。
env — 声明影响缓存的环境变量
{
"tasks": {
"build": {
"env": ["NODE_ENV", "API_URL"]
}
}
}⚠️ 漏声明会导致错误的缓存复用:本地 API_URL=dev 构建的产物被 CI 用 API_URL=prod 命中缓存。规则是「所有非 PATH 的环境变量默认都参与哈希」,但显式声明更清晰。
cache: false — 禁用缓存
{
"tasks": {
"dev": { "cache": false }
}
}常用于 dev、preview 这类不该缓存的任务。
persistent: true — 长驻任务
{
"tasks": {
"dev": { "cache": false, "persistent": true }
}
}turbo run dev 需要同时起多个 dev server,标记为 persistent 让 Turborepo 知道这个任务不会结束,不做依赖图剪枝(改一个包也要全起)。
🔗 依赖图怎么来的
Turborepo 只看 package.json 的依赖关系,不猜。
// packages/ui/package.json
{
"name": "@my/ui",
"dependencies": { "@my/utils": "workspace:*" }
}有这行依赖 → ui 依赖 utils → turbo run build 时 utils 先跑。
注意:
devDependencies也算依赖peerDependencies不算(可选with调整)- 本地路径依赖(
file:../utils)也认,但workspace:*更规范
with — 扩展依赖图
需要把某些包也算进依赖图(比如运行时读的配置文件、需要一起构建的 WASM):
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"with": ["@my/config"]
}
}
}🎯 Filter 过滤
控制「跑哪些包」,是 Turborepo 最省时间的部分。
# 单个包
turbo run build --filter=web
# 目录 glob
turbo run build --filter=./apps/*
# 排除
turbo run build --filter=!web
# 该包 + 它依赖的所有上游
turbo run build --filter=...web
# 自上次 commit 起受影响的包 + 其下游
turbo run build --filter=...[HEAD^1]
# 与 main 分支对比的受影响范围
turbo run build --filter=...[origin/main]
# 排除某种改动(假定文档改动不该触发构建)
turbo run build --filter=...[origin/main] --filter=!./docs/**记忆法:
| 写法 | 含义 |
|---|---|
web | 只有 web |
...web | web 和它的上游(依赖链) |
web... | web 和它的下游(依赖它的) |
...[ref] | 相对 ref 有改动的包 + 其下游 |
...web | (同上) |
💾 缓存机制
哈希怎么算
Turborepo 对每个任务算一个哈希,输入包含:
- 任务定义本身(
turbo.json里这个 task 的配置) inputs范围内的文件内容哈希- 上游包的哈希(上游变了,本地必然失效)
env声明的环境变量- 依赖版本(
package.json/ lockfile)
任意一项变了 → 哈希变 → 缓存失效。全命中则跳过任务,直接还原 outputs。
缓存存哪
| 位置 | 路径 | 说明 |
|---|---|---|
| 本地 | .turbo/cache/ | 默认位置,一般要 gitignore |
| 远程 | Vercel 托管 | 团队共享,CI 和本地都能命中 |
远程缓存配置
# 本地登录
npx turbo login
npx turbo link
# CI 环境用环境变量(推荐,不要在 CI 里跑交互命令)
export TURBO_TOKEN=xxx
export TURBO_TEAM=my-team.gitignore 里加上:
.turbo
📊 框架集成(v2 新特性)
v2 提供了官方框架包,省掉手写 outputs 和框架特定配置:
pnpm add -D @turbo/nextjs
# 或
pnpm add -D @turbo/vite{
"extends": ["@turbo/nextjs/config"]
}包内用 preset:
{
"extends": ["//"]
}可用的:@turbo/nextjs、@turbo/vite、@turbo/astro、@turbo/svelte、@turbo/angular、@turbo/webpack
🧩 v2 的 prune:CI 瘦身
Monorepo 的痛点:CI 上要装全部依赖,但可能只改了一个 app。turbo prune 可以把仓库裁剪成「只包含相关包」的子集:
turbo prune web --docker
# 生成:
# out/
# ├── apps/web/ # 只要这个 app
# ├── packages/ui/ # 以及它依赖的
# ├── package.json
# ├── pnpm-lock.yaml # 裁剪后的 lockfile
# └── turbo.jsonCI 里就能只装必要依赖,Docker 镜像层也小很多。
⚠️ 常见坑
1. 忘记写 outputs → 缓存了但下游拿不到文件
症状:缓存显示命中,但下游包报「找不到模块」。
原因:Turborepo 存了日志和退出码,但没存构建产物。
解决:老老实实声明每个 task 的 outputs。
2. 长驻任务忘标 persistent → 改一个包要全起
dev 没标 persistent: true,Turborepo 以为它会结束,做了依赖剪枝,改 A 包时 B 的 dev server 不起。
3. 环境变量漏声明 → CI 用了本地产物
本地 .env.production 里的值参与了构建,但没写进 env,CI 命中本地缓存拿到错误产物。排查方式是 --dry=json 看哈希输入。
4. devDependencies 造成的幽灵依赖图
A 的 devDeps 里有 B → A 依赖 B。即使 A 的运行时不需要 B,它也进了依赖图,^build 会等 B 构建完。
5. 缓存目录误提交
.turbo/ 要 gitignore。提交上去会让 CI 拉到别人的缓存,行为不可控。
6. 远程缓存没有配 token → 命中率低
本地 turbo login 了但 CI 没设 TURBO_TOKEN,CI 全部 miss。CI 和本地是两套缓存空间。
7. pipeline vs tasks 混用
v1 写 pipeline,v2 读 tasks。升级时把 pipeline 改名成 tasks,其余不动。
🔍 调试与排查
# 看看会跑哪些任务、为什么跑
turbo run build --dry=json > plan.json
# 只跑某个包,跳过依赖
turbo run build --filter=web --no-deps
# 强制绕过缓存重跑
turbo run build --force
# 清空本地缓存
rm -rf .turbo/cache
# 详细日志
turbo run build --verbosity=2
# 跑一次并解释每个任务是否命中缓存
turbo run build --summarize--summarize 会生成 .turbo/runs/<id>.json,里面有每个任务的 cache 字段(HIT / MISS / LOCAL / REMOTE),排查「为什么没命中」用这个。
⚖️ 与其他工具对比
| 方案 | 任务编排 | 缓存 | 生态复杂度 | 适用 |
|---|---|---|---|---|
| Turborepo | ✅ 依赖图 | ✅ 本地+远程 | 低 | 纯 JS/TS monorepo |
| Nx | ✅ 更强(图/规则/生成器) | ✅ | 高 | 复杂多语言、需要代码生成 |
| Lerna | ⚠️ 有限 | ❌ 弱 | 中 | 主要做版本管理 |
pnpm -r | ⚠️ 顺序 | ❌ | - | 小规模够用 |
| Nx / Turborepo 混合 | - | - | - | 少见,不推荐 |
选择建议:
- 包 ≤ 5 个、构建简单 →
pnpm -r够了,别过度设计 - 纯 JS/TS、想省事 → Turborepo
- 复杂 monorepo、需要代码生成/多语言 → Nx
📋 迁移路径
从「无编排的 monorepo」迁到 Turborepo:
- 确认 workspace 正常 —
pnpm-workspace.yaml配好,pnpm install无报错 - 各包补齐
name和版本 — Turborepo 靠name匹配 filter - 加
turbo依赖 + scripts — 根package.json - 写
turbo.json— 从最基础的tasks: { "build": { "outputs": [] } }开始 - 逐个任务补
outputs— 先 build,再 test,再 lint turbo run build对比pnpm -r build— 产物应该一致- 配远程缓存 —
turbo login/ CI 设TURBO_TOKEN - 接
turbo prune到 CI(可选)— 大仓库收益明显 - 清理旧脚本 — 原来手写的
&& npm run build串联可以删了
🔗 相关
最后更新:2026-10-05