WEBKT

从 30 分钟到 3 分钟:Monorepo 下的 Turborepo 缓存加速实践

4 0 0 0

在现代前端工程中,Monorepo 架构(如使用 pnpm 或 Yarn Workspaces)已成为中大型项目的首选。然而,随着子项目(Packages)数量的增加,CI/CD 流程往往会陷入“构建泥潭”:哪怕只是改动了一个工具函数的注释,流水线也要把几十个 App 全部重新跑一遍测试和构建。

Turborepo 的出现彻底改变了这一局面。它通过极简的配置和强大的缓存机制,让“增量构建”真正落地。本文将深入探讨如何在 CI 流程中压榨 Turborepo 的性能,实现构建效率的质变。

一、 核心逻辑:为什么 Turborepo 快?

Turborepo 的核心思想是**“不重复做已经做过的工作”**。它为每个任务生成一个基于以下内容的唯一哈希值(Fingerprint):

  1. 源文件内容inputs 中定义的文件)
  2. 依赖项package.json 中的依赖)
  3. 环境变量(控制不同环境的构建逻辑)
  4. 子项目间的依赖树

当任务执行时,如果哈希值匹配且本地或远程存在对应的缓存,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 目录会包含:

  1. json/:仅包含依赖项的 package.json(用于先运行 pnpm install 锁定层缓存)。
  2. 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

五、 避坑指南

  1. 环境变量陷阱:如果在构建脚本中注入了 API_KEY,记得在 turbo.jsonglobalEnv 或任务的 env 中声明它。否则,即便环境变量变了,Turborepo 可能还会给你过期的缓存。
  2. 非确定性输出:确保你的构建过程是确定性的。如果代码中包含 Date.now() 并在构建时写入文件,哈希值每次都会变,缓存将毫无意义。
  3. 忽略本地缓存:在排查诡异构建问题时,可以使用 --no-cache 暂时关闭缓存,确定是代码问题还是缓存污染。

总结

引入 Turborepo 并不是简单的增加一个工具,而是对 Monorepo 开发流的一次重塑。通过精细化的 pipeline 配置和远程缓存的引入,我们不仅缩短了 CI 时间,更大幅提升了开发者的本地幸福感。在如今追求效率的工程环境下,这无疑是 Monorepo 优化的必经之路。

架构师老路 TurborepoMonorepoCICD优化

评论点评