🏎️ 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
Vitedist/**
Next.js.next/**, !.next/cache/**
webpackdist/**
tsclib/** 或 dist/**/*.tsbuildinfo
Rusttarget/**

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
...webweb 和它的上游(依赖链)
web...web 和它的下游(依赖它的)
...[ref]相对 ref 有改动的包 + 其下游
...web(同上)

💾 缓存机制

哈希怎么算

Turborepo 对每个任务算一个哈希,输入包含:

  1. 任务定义本身(turbo.json 里这个 task 的配置)
  2. inputs 范围内的文件内容哈希
  3. 上游包的哈希(上游变了,本地必然失效)
  4. env 声明的环境变量
  5. 依赖版本(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.json

CI 里就能只装必要依赖,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:

  1. 确认 workspace 正常 — pnpm-workspace.yaml 配好,pnpm install 无报错
  2. 各包补齐 name 和版本 — Turborepo 靠 name 匹配 filter
  3. 加 turbo 依赖 + scripts — 根 package.json
  4. 写 turbo.json — 从最基础的 tasks: { "build": { "outputs": [] } } 开始
  5. 逐个任务补 outputs — 先 build,再 test,再 lint
  6. turbo run build 对比 pnpm -r build — 产物应该一致
  7. 配远程缓存 — turbo login / CI 设 TURBO_TOKEN
  8. 接 turbo prune 到 CI(可选)— 大仓库收益明显
  9. 清理旧脚本 — 原来手写的 && npm run build 串联可以删了

🔗 相关


最后更新:2026-10-05