FumadocsZDecode
工程化

Monorepo 构建缓存原理与配置

构建缓存的本质是:用任务的输入指纹(hash)作 key,把任务的输出(产物 + 终端日志)作 value 存起来。下次构建时指纹命中就直接复用,跳过执行。 配合任务依赖图(DAG),它能保证「上游变了下游一定重建、上游没变就吃缓存」。


一、核心原理

1. 输入指纹(Hash)怎么算

一个任务的 hash 由这些输入组合而成:

hash = SHA(源文件 + 配置 + env + 命令 + 依赖项的 hash)
输入说明
源代码该 package 内参与构建的文件内容
环境变量任务依赖的 env(如 NODE_ENV
命令build 脚本本身的内容
工具链版本Node、构建工具版本
依赖包的 hash关键:上游依赖变了,下游缓存必须失效

2. 缓存命中流程

3. 两个支撑机制

  • 任务依赖图(Task Graph):根据 package.json 依赖关系和 pipeline 配置构建 DAG,决定执行顺序和 hash 的传递。app 依赖 ui,则 ui 的 hash 会进入 app 的 hash 计算。
  • 输入裁剪:只有声明的 inputs 变化才让 hash 变化。改 README 不该使构建缓存失效,靠精确声明输入来实现。

二、本地缓存 vs 远程缓存

本地缓存远程缓存
存储位置node_modules/.cache 等磁盘目录远端(Vercel、自建 S3)
受益对象单台机器、单人整个团队 + CI
典型场景本地反复构建CI 复用同事 / 上次 CI 的产物

远程缓存是团队 / CI 场景提速的关键:A 构建过的 package,B 和 CI 可以直接拉产物。


三、配置(以 Turborepo 为例)

turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json", "package.json"],
      "outputs": ["dist/**"],
      "env": ["NODE_ENV"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
字段含义
dependsOn: ["^build"]^ 表示先构建所有上游依赖,声明 DAG 拓扑顺序,并让上游 hash 进入下游计算
inputs声明参与 hash 的文件,裁剪无关文件(README 改动不触发重建)
outputs声明产物路径,命中时从缓存还原;漏声明会导致命中后产物缺失
env环境变量纳入 hash,避免 NODE_ENV 不同却复用错误产物
cache: false / persistent: truedevwatch 这类长驻或无产物任务不缓存

远程缓存接入:

turbo login
turbo link        # 关联远程缓存
turbo run build   # CI 自动读写远程 cache

四、易错点与对比

缓存失效的常见坑

  • outputs 漏声明 → 命中后 dist 是空的。
  • 全局环境变量没纳入 globalDependencies → 改了 .env 仍命中旧缓存。
  • 锁文件(pnpm-lock.yaml)变化应让依赖相关任务失效。

Turborepo vs Nx

两者原理一致(hash + task graph + remote cache)。Nx 额外有「受影响项目检测」(nx affected),基于 Git diff 只跑受影响的项目,与缓存配合更激进;Turborepo 更轻量、配置更简单。

为什么不只要缓存,还要任务依赖图

缓存解决「要不要重复执行」,task graph 解决「按什么顺序执行 + hash 如何传递」。两者结合才能保证上游变了下游一定重建,不会用到脏缓存。


一句话记忆:构建缓存 = 输入 hash 当 key、产物与日志当 value;任务依赖图让上游 hash 渗透到下游,从而「没变吃缓存、变了必重建」;远程缓存把这份复用从单人扩展到整个团队与 CI。

On this page