hash 的输入集合
任务 @myorg/ui#build 的 hash = SHA-256 of: ├── 包源码文件内容(根据 inputs 过滤) ├── package.json ├── lockfile 里该包相关的版本信息 ├── 依赖包的 hash(recursive) ├── 声明的 env 变量的值 ├── globalDependencies 里的文件内容 ├── globalEnv 的值 ├── turbo.json 里该任务的配置 └── 执行的 script 命令
任一输入变化 → hash 变 → 缓存 miss。
FULL TURBO 的含义
Tasks: 10 successful, 10 total Cached: 10 cached, 10 total Time: 422ms >>> FULL TURBO
10 个任务全部命中缓存,真正执行的只是"从 .turbo 拷贝 dist 产物 + 回放 stdout 日志"——几乎是文件复制的速度。
部分命中
@myorg/utils:build: cache hit, replaying logs @myorg/ui:build: cache miss, executing ← utils 改了,ui 跟着失效 @myorg/web:build: cache miss, executing @myorg/docs:build: cache hit, replaying logs ← docs 不依赖 ui 的某些文件,可能仍命中
改 utils 会让依赖 utils 的包一起 miss——DAG 的传染。
精细 inputs 提高命中率
// 默认 { "build": { "inputs": ["$TURBO_DEFAULT$"] } } // 改 README 也失效缓存 ← 浪费 // 更好 { "build": { "inputs": [ "src/**", "tsconfig.json", "package.json" ] } } // 只在这些目录变化时才重新 build
精细 inputs 是命中率优化的最大杠杆
默认包含整个包的所有文件——意味着 README/测试/文档改动也会失效缓存。显式列 inputs 把范围收窄到真正影响产物的文件,提高命中率立竿见影。
默认包含整个包的所有文件——意味着 README/测试/文档改动也会失效缓存。显式列 inputs 把范围收窄到真正影响产物的文件,提高命中率立竿见影。
outputs 要写全
{
"build": {
"outputs": [
"dist/**",
".next/**",
"!.next/cache/**", // 不要缓存 Next 自己的缓存
"public/generated/**"
]
}
}
outputs 是"要恢复的产物路径"。漏了会怎样?
- 第一次 build 生成了 dist + public/generated
- turbo.json 只列了 dist → 只有 dist 进缓存
- 第二次 build 命中缓存 → 只恢复 dist → public/generated 丢了 → 运行时 404
缓存 key 的稳定性
# 看某任务的 hash pnpm turbo run build --dry-run=json | jq '.tasks[] | select(.taskId=="@myorg/ui#build") | .hash' # "5a2f1e8c3d7f..."
连跑两次没改代码,hash 应该完全一致。如果变了——说明某个"不该影响"的输入被算进去了,往下查。
常见 hash 抖动原因
时间戳进了产物
webpack banner 含 build time,outputs 的文件内容每次不同——导致下游任务 hash 变(因为这个包的 hash 依赖它的 outputs)。修复:去掉时间戳,或把含时间戳的文件从 outputs 排除。
没锁 Node 版本
Node 不同小版本 build 出的二进制可能不同。用 Corepack + packageManager 锁。
.env 变量泄漏
任务意外读取了 process.env.SOMETHING 但没声明在 env 里——这是错误的,Turbo 无法感知。声明进 env,或改代码不读它。
lockfile 频繁变
每次升级依赖都会改 lockfile → 所有任务 hash 变。这是对的——只是改依赖就该重跑。
本地缓存位置
# 默认 apps/web/.turbo/cache/ ├── 5a2f1e8c3d7f9a2b.tar.zst ← 产物压缩包 ├── 5a2f1e8c3d7f9a2b-meta.json ← 元数据 └── 5a2f1e8c3d7f9a2b.log ← stdout/stderr # 清本地缓存 rm -rf apps/web/.turbo/cache # 下次 build 会重新生成
替代缓存目录
pnpm turbo run build --cache-dir=.cache/turbo # 自定义缓存位置 pnpm turbo run build --no-cache # 本轮不用缓存也不写缓存 pnpm turbo run build --force # 忽略缓存,强制重跑,但会写新缓存
缓存签名(防篡改)
{
"remoteCache": {
"signature": true
}
}
.env
TURBO_REMOTE_CACHE_SIGNATURE_KEY=a-secret-random-key
开启后,每个缓存 tar 包附带 HMAC 签名——下载时不对就拒绝。防止远程缓存服务器被攻击后投毒。
cache-miss 分析工作流
# 1. 保存第一次 run 的 hash pnpm turbo run build --dry-run=json > run1.json # 2. 改个不应该影响缓存的文件(比如 README) echo "x" >> README.md # 3. 再跑一次 pnpm turbo run build --dry-run=json > run2.json # 4. diff 两次 hash diff <(jq -r '.tasks[] | "\(.taskId): \(.hash)"' run1.json) \ <(jq -r '.tasks[] | "\(.taskId): \(.hash)"' run2.json)
如果 hash 变了 → 说明 README 被 inputs 包含——收窄 inputs 排除之。
任务级别的 env
{
"build": {
"env": [
"NEXT_PUBLIC_API_URL",
"NEXT_PUBLIC_*", // 通配
"!NEXT_PUBLIC_DEBUG" // 通配后排除
]
}
}
Turbo 会读声明过的 env 值,算进 hash。未声明的 env 不影响缓存——这就是为什么 env 必须显式声明。
replayLog 的原理
cache hit 时,Turbo 做两件事:
- 从 .turbo/cache 解压 tar.zst 到 outputs 指定的目录
- 把上次的 stdout/stderr 日志原样打印出来(用色彩做"replay"标识)
结果:看起来像重跑了,实际是纯文件复制 + echo,快到毫秒级。
远程缓存和本地缓存
任务请求: 1. 本地 .turbo/cache 有 hash xxx ? → 用 2. 没有 → 查远程缓存(Vercel/S3) 3. 也没有 → 执行任务 4. 执行完 → 写本地 + 上传远程(如果启用)
下一章详细讲远程缓存。
summarize 模式
pnpm turbo run build --summarize
{
"id": "xxx",
"version": "1",
"turboVersion": "2.2.3",
"executionSummary": { "success": 10, "cached": 8, "failed": 0, "attempted": 10 },
"tasks": [
{
"taskId": "@myorg/ui#build",
"hash": "5a2f1e8c...",
"cacheState": { "local": true, "remote": false },
"executionTime": 12,
"expandedInputs": { "src/index.ts": "...", ... }
}
]
}
生成的 JSON 可以投喂到监控 dashboard——跟踪每次 CI 的缓存命中率,量化优化效果。
命中率调优 checklist
显式 inputs 收窄范围
不要默认
$TURBO_DEFAULT$,列具体目录。outputs 写全
漏了会运行时出 bug,缓存却命中。
env 显式声明
不声明 = 不进 hash = 可能错误命中。
消除非确定性
产物里不要有时间戳、随机 ID、node_modules 的绝对路径。
Node 版本锁定
Corepack + packageManager,避免 Node 差异造成产物差异。
监控 --summarize
定期看哪些任务命中率低,针对性优化。
本章小结
- 任务 hash = 源码 + package.json + lockfile + 依赖 hash + env + 全局依赖
- FULL TURBO 表示所有任务命中缓存,时间降到毫秒级
- 收窄 inputs、写全 outputs 是命中率优化的两大杠杆
- 消除非确定性(时间戳/随机 ID)让产物可复现
--dry-run=json+--summarize是调试缓存的主力工具