从 30 分钟到 3 分钟:Monorepo 下的 Turborepo 缓存加速实践
在现代前端工程中,Monorepo 架构(如使用 pnpm 或 Yarn Workspaces)已成为中大型项目的首选。然而,随着子项目(Packages)数量的增加,CI/CD 流程往往会陷入“构建泥潭”:哪怕只是改动了一个工具函数的注释,流水线也要把几十个 App 全部重新跑一遍测试和构建。
Turborepo 的出现彻底改变了这一局面。它通过极简的配置和强大的缓存机制,让“增量构建”真正落地。本文将深入探讨如何在 CI 流程中压榨 Turborepo 的性能,实现构建效率的质变。
一、 核心逻辑:为什么 Turborepo 快?
Turborepo 的核心思想是**“不重复做已经做过的工作”**。它为每个任务生成一个基于以下内容的唯一哈希值(Fingerprint):
- 源文件内容(
inputs中定义的文件) - 依赖项(
package.json中的依赖) - 环境变量(控制不同环境的构建逻辑)
- 子项目间的依赖树
当任务执行时,如果哈希值匹配且本地或远程存在对应的缓存,Turborepo 会直接跳过执行过程,瞬间“恢复”输出文件(如 dist 目录)和终端日志。
二、 关键配置:编写高效的 turbo.json
要让缓存生效,首先要正确定义任务依赖。以下是一个典型的 turbo.json 配置示例:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
// build 任务依赖于父级或依赖包的 build 任务完成
"dependsOn": ["^build"],
// 定义哪些文件的产物需要被缓存
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
// 只有这些文件的变动会触发重新构建
"inputs": ["src/**", "public/**", "next.config.js"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}
技巧点:
dependsOn: ["^build"]:这里的^符号表示依赖拓扑结构。只有当所有的依赖包(Dependencies)都构建完成了,当前包才会开始构建。- 精确的
inputs:不要把README.md或.pnpm-debug.log包含在内,否则修改文档也会导致缓存失效。
三、 CI 环境下的缓存持久化
CI 环境(如 GitHub Actions、GitLab CI)通常是临时构建机,这意味着本地缓存(node_modules/.cache/turbo)在每次运行后都会被销毁。要利用缓存,有两种方案:
1. 使用 GitHub Actions 自带缓存
通过 actions/cache 手动保存缓存文件夹:
- name: Cache Turbo
uses: actions/cache@v3
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Build
run: pnpm build --cache-dir=".turbo"
2. 使用远程缓存(Remote Caching)—— 最强方案
这是 Turborepo 最引以为傲的功能。它允许开发者或 CI 将缓存上传到云端(如 Vercel 提供的免费服务或自建的 turbo-rsc)。
在 CI 脚本中只需一行配置:
# 自动识别远程缓存,无需手动 actions/cache
npx turbo build --token=${{ secrets.TURBO_TOKEN }} --team=${{ secrets.TURBO_TEAM }}
优势: 不仅 CI 能用,团队里其他同事在本地拉取代码后,也能直接复用 CI 生成的构建产物,真正实现“全员加速”。
四、 进阶优化:利用 turbo prune 优化 Docker 构建
在 Monorepo 中构建 Docker 镜像时,最头疼的是 COPY . . 会把整个仓库几百个包全拷贝进去,导致镜像体积巨大且层缓存频繁失效。
Turborepo 提供了 prune 工具,可以将特定 App 及其依赖“修剪”出来:
# 提取名为 "web" 的应用及其依赖到 out 目录
npx turbo prune --scope=web --docker
执行后,out 目录会包含:
json/:仅包含依赖项的package.json(用于先运行pnpm install锁定层缓存)。full/:实际的代码文件。
Dockerfile 优化实践:
FROM node:18-alpine AS builder
WORKDIR /app
RUN npm install -g turbo
COPY . .
RUN turbo prune --scope=web --docker
FROM node:18-alpine AS runner
WORKDIR /app
# 先复制 json,利用 Docker 层缓存安装依赖
COPY --from=builder /app/out/json/ .
RUN pnpm install
# 再复制源码进行构建
COPY --from=builder /app/out/full/ .
RUN npx turbo build --filter=web
五、 避坑指南
- 环境变量陷阱:如果在构建脚本中注入了
API_KEY,记得在turbo.json的globalEnv或任务的env中声明它。否则,即便环境变量变了,Turborepo 可能还会给你过期的缓存。 - 非确定性输出:确保你的构建过程是确定性的。如果代码中包含
Date.now()并在构建时写入文件,哈希值每次都会变,缓存将毫无意义。 - 忽略本地缓存:在排查诡异构建问题时,可以使用
--no-cache暂时关闭缓存,确定是代码问题还是缓存污染。
总结
引入 Turborepo 并不是简单的增加一个工具,而是对 Monorepo 开发流的一次重塑。通过精细化的 pipeline 配置和远程缓存的引入,我们不仅缩短了 CI 时间,更大幅提升了开发者的本地幸福感。在如今追求效率的工程环境下,这无疑是 Monorepo 优化的必经之路。