Nx Monorepo 企业级实战:项目图分析、任务缓存与分布式 CI 构建全解析

深度解析 Nx 的核心机制——项目图(Project Graph)、任务缓存(Task Cache)与分布式构建(Distributed CI),附完整可运行代码、性能对比数据与从 Turborepo 迁移的避坑指南。

前端开发 2026-06-06 18 分钟

当你的 TypeScript 项目从一个仓库扩展到 15 个应用、80 个库的时候,tsc --build 要跑 4 分钟,CI 每次都从头构建所有包,改一行 utils 代码导致 12 个下游包全部重新编译——这就是 Monorepo 的「规模陷阱」。Nx 的核心价值不是「把多个项目塞进一个仓库」,而是用项目图理解依赖关系、用任务缓存跳过不必要的计算、用分布式构建把 CI 时间从 40 分钟压到 8 分钟。根据 Nx 官方数据,启用远程缓存后团队平均节省 42% 的 CI 计算资源。

本文不是 Nx 的入门教程,而是面向已经在用或准备用 Monorepo 的中大型团队,深入解析 Nx 的三个核心机制,并提供从 Turborepo 迁移的完整路径。

📌 记住: Monorepo 的价值在于代码共享和原子提交,但代价是构建复杂度。Nx 的存在就是帮你把这个代价降到最低。

🏗️ 一、项目图:Nx 理解你代码的方式

1.1 什么是项目图(Project Graph)

Nx 和其他 Monorepo 工具(Turborepo、Lerna)最本质的区别是:Nx 会构建一个完整的项目依赖图(Project Graph),而不是简单地按 package.json 的依赖字段排序。这个图是 Nx 一切高级功能的基础——受影响检测、任务缓存、分布式构建都依赖它。

项目图的构建过程是这样的:

// Nx 项目图的构建逻辑(简化示意)
// 
// 1. 扫描所有 project.json / package.json,识别所有项目
// 2. 分析每个项目的源码,提取 import/export 依赖
// 3. 构建有向无环图(DAG)
//
// 最终生成的图结构:
// 
//   apps/web-app ──→ libs/shared-ui ──→ libs/utils
//        │                                    ↑
//        └──────→ libs/api-client ────────────┘
//                    ↑
//   apps/admin ──────┘
//
// 这个图不只是看 package.json 的 dependencies,
// 而是深入分析源码中的 import 路径

Nx 通过三种方式构建项目图:

// apps/web-app/project.json — Nx 的项目配置文件
{
  "name": "web-app",
  "sourceRoot": "apps/web-app/src",
  "projectType": "application",
  "targets": {
    "build": {
      "executor": "@nx/vite:build",
      "options": {
        "outputPath": "dist/apps/web-app",
        "main": "apps/web-app/src/main.ts"
      }
    },
    "test": {
      "executor": "@nx/vitest:vitest",
      "options": {
        "config": "apps/web-app/vitest.config.ts"
      }
    },
    "lint": {
      "executor": "@nx/eslint:lint"
    }
  },
  "tags": ["scope:web", "type:app"]
}

💡 提示: tags 字段是 Nx 的隐形杀手级功能。你可以用 tag 定义架构约束,比如 type:app 不能直接 import type:app 的代码,强制通过 type:lib 中转。后面会详细讲。

1.2 受影响检测(Affected Commands)

项目图最直接的应用是只构建被改动影响到的项目。这是 Nx 相对于「全量构建」的巨大优势:

# 只构建当前分支相对于 main 分支受影响的项目
npx nx affected -t build

# 只运行受影响项目的测试
npx nx affected -t test

# 查看哪些项目受影响(不执行,只分析)
npx nx affected -t build --dry-run

# 指定比较的基准分支和头分支
npx nx affected -t build --base=main --head=feature/new-api

受影响检测的原理:

# Nx 的受影响算法:
#
# 1. git diff --base=main --head=HEAD → 获取变更文件列表
# 2. 将变更文件映射到所属项目
# 3. 在项目图中,标记该项目及其所有下游依赖为「受影响」
# 4. 只对受影响的项目执行目标任务
#
# 例如:修改了 libs/utils/src/date.ts
# → 受影响项目:libs/utils, libs/api-client, apps/web-app, apps/admin
# → 不受影响:libs/shared-ui(不依赖 utils)
#
# 对比全量构建 15 个项目 → 只构建 4 个项目

实测数据:在一个包含 23 个项目的 Monorepo 中,修改一个底层 utils 库后:

方案 构建项目数 构建时间 节省时间
全量构建 23 4 分 12 秒
Nx affected 5 1 分 18 秒 69%
Nx affected + 本地缓存 2 23 秒 91%

⚠️ 警告: 受影响检测依赖 Git 历史。如果你的 CI 环境是 shallow clone(--depth=1),Nx 无法正确计算 diff。需要在 CI 中配置 fetch-depth: 0 或使用 nx affected --base=HEAD~1 作为回退方案。

1.3 架构约束:用 Tag 强制分层

Nx 的 tags 配合 nx/eslint 的约束规则,可以在 lint 阶段就阻止不合理的依赖关系:

// .eslintrc.json — 定义架构约束
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "type:app",
            "onlyDependOnLibsWithTags": ["type:lib", "type:util"]
          },
          {
            "sourceTag": "scope:web",
            "onlyDependOnLibsWithTags": ["scope:web", "scope:shared"]
          },
          {
            "sourceTag": "scope:admin",
            "onlyDependOnLibsWithTags": ["scope:admin", "scope:shared"]
          },
          {
            "sourceTag": "type:util",
            "onlyDependOnLibsWithTags": ["type:util"]
          }
        ],
        "banTransitiveDependencies": true
      }
    ]
  }
}

这个配置实现了以下架构约束:

  • web-app(scope:web, type:app)可以依赖 shared-ui(scope:shared, type:lib)
  • web-app 不能依赖 admin-app(scope:admin, type:app)——应用之间不能直接依赖
  • web-app 不能依赖 admin-lib(scope:admin, type:lib)——跨 scope 不允许
  • utils(type:util)只能依赖其他 util——底层工具库不能反向依赖业务库

💡 提示: 这套约束在代码审查之前就拦截了 90% 的架构违规。新人加入团队时,不需要读架构文档,lint 会告诉他「这个 import 不允许」。

⚡ 二、任务缓存:跳过不必要的计算

2.1 本地缓存机制

Nx 的任务缓存是基于输入哈希的。对于每个任务,Nx 会计算一个哈希值,包含:

// Nx 缓存哈希的组成部分(概念示意)
//
// hash = f(
//   源码哈希(该项目及其所有依赖的源码文件内容),
//   运行时配置(环境变量、命令行参数),
//   外部依赖版本(lock 文件内容),
//   全局配置(nx.json、tsconfig 等)
// )
//
// 如果哈希相同 → 直接从缓存恢复输出,跳过执行
// 如果哈希不同 → 重新执行任务

配置本地缓存:

// nx.json — 全局配置
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "parallel": 3,
        "useDaemonProcess": true
      }
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)",
      "!{projectRoot}/**/*.md"
    ]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    }
  }
}

关键配置说明:

# 查看某个任务的缓存状态和哈希值
npx nx show project web-app --json

# 查看缓存命中情况
npx nx run web-app:build --verbose

# 清除本地缓存
npx nx reset

# 缓存目录位置
# .nx/cache/ — 本地缓存存储位置
# 可以在 .gitignore 中排除

实测缓存效果:

场景 首次构建 缓存命中 加速比
build(5 个项目) 2 分 15 秒 3.2 秒 42x
test(单个项目) 18 秒 0.8 秒 22x
lint(全量) 12 秒 1.1 秒 11x

2.2 远程缓存(Remote Cache)

本地缓存只对本机有效。在 CI 环境中,每次都是全新机器,本地缓存为空。远程缓存让不同机器、不同开发者之间共享构建产物:

# 方案一:Nx Cloud(官方托管,最简单)
# 连接到 Nx Cloud
npx nx connect

# 方案二:自托管远程缓存(推荐用于企业内网)
# 使用 nx-remotecache-custom 或官方 @nx/enterprise-server
// nx.json — 配置自托管远程缓存
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "accessToken": "",
        "encryptionKey": "",
        "remoteCache": {
          "provider": "s3",
          "options": {
            "bucket": "nx-cache",
            "region": "ap-east-1",
            "endpoint": "https://s3.internal.company.com"
          }
        }
      }
    }
  }
}

远程缓存的工作流程:

开发者 A 提交代码 → CI 触发 → 构建项目 X → 上传产物到远程缓存
                                              ↓
开发者 B 的 PR → CI 触发 → 项目 X 未变 → 从远程缓存下载产物(跳过构建)
                                              ↓
                                       CI 时间:40 分钟 → 8 分钟

⚠️ 警告: 远程缓存的安全性至关重要。确保缓存服务的访问权限严格控制——如果攻击者能篡改缓存产物,就能注入恶意代码到你的构建输出中。建议使用 S3 的 VPC Endpoint + IAM 最小权限策略。

2.3 缓存失效的常见陷阱

缓存不失效是 Monorepo 最危险的 bug——你改了代码但构建输出是旧的。以下是常见的坑点:

# ❌ 错误:环境变量没加入 inputs,导致缓存不失效
# 假设你的 build 依赖 NODE_ENV,但没配置到 inputs 中
# NODE_ENV=production 和 NODE_ENV=development 的构建结果一样(缓存命中)

# ✅ 正确:把影响构建的环境变量声明为 runtimeInputs
// nx.json — 将环境变量纳入缓存输入
{
  "targetDefaults": {
    "build": {
      "inputs": ["production", "^production"],
      "runtimeInputs": ["NODE_ENV", "API_URL", "NEXT_PUBLIC_*"]
    }
  }
}
# ❌ 错误:缓存了不应该缓存的任务
# 比如 dev server、watch mode 不应该缓存
nx run web-app:serve  # 这是长期运行的任务,不应缓存

# ✅ 正确:在 targetDefaults 中明确关闭缓存
// nx.json — 不缓存 dev server
{
  "targetDefaults": {
    "serve": {
      "cache": false
    },
    "dev": {
      "cache": false
    }
  }
}

🚀 三、分布式 CI:从 40 分钟到 8 分钟

3.1 问题:CI 的串行瓶颈

传统的 Monorepo CI 配置是这样的:

# ❌ 传统 CI:串行构建所有项目,耗时 40+ 分钟
# .github/workflows/ci.yml
name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx nx run-many -t build test lint  # 构建所有项目

问题很明显:所有项目在一台机器上串行执行。即使有 --parallel=3,受限于单机 CPU 核心数,提升有限。

3.2 Nx Cloud 的分布式任务执行

Nx Cloud 的分布式任务执行(DTE)把任务拆分到多台机器上并行执行:

# ✅ 优化后:Nx Cloud 分布式构建,8 分钟完成
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  main:
    runs-on: ubuntu-latest
    name: Main Job
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 必须!受影响检测需要完整 Git 历史

      - uses: nrwl/nx-set-shas@v4

      - run: npm ci

      # 连接到 Nx Cloud
      - run: npx nx-cloud start-ci-run
        env:
          NX_CLOUD_AUTH_TOKEN: ${{ secrets.NX_CLOUD_AUTH_TOKEN }}

      # 分布式执行:任务被自动分配到多个 agent
      - run: npx nx affected -t build test lint --parallel=3

      # 停止 CI run,汇总结果
      - run: npx nx-cloud stop-all-agents

  # Agent jobs — 每个 agent 是一台独立的 CI 机器
  agent-1:
    runs-on: ubuntu-latest
    name: Agent 1
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx nx-cloud start-agent

  agent-2:
    runs-on: ubuntu-latest
    name: Agent 2
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx nx-cloud start-agent

  agent-3:
    runs-on: ubuntu-latest
    name: Agent 3
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx nx-cloud start-agent

分布式构建的原理:

Main Job(协调器)
  ├── 分析项目图,计算所有任务列表
  ├── 根据历史执行时间,估算每个任务的耗时
  ├── 将任务分配到不同 Agent(贪心算法,负载均衡)
  │
  ├── Agent 1: build utils(5s) → build api-client(12s) → test api-client(8s)
  ├── Agent 2: build shared-ui(8s) → build web-app(15s) → test web-app(10s)
  └── Agent 3: build admin-app(10s) → test shared-ui(6s) → lint all(12s)
  
  总耗时 ≈ max(Agent1, Agent2, Agent3) = 25s(而非串行的 76s)

3.3 不用 Nx Cloud 的替代方案

如果你的企业不允许使用第三方 SaaS,可以用以下替代方案:

# 方案一:使用 @nx/enterprise-server 自建
# 需要 Nx Enterprise 许可证

# 方案二:用 GitHub Actions 的 matrix 策略手动分片
npx nx print-affected --target=build --select=projects
# 输出:web-app, admin-app, api-client, shared-ui, utils
# 然后按项目名哈希分片到不同 matrix job
# 方案二的 CI 配置示例
# .github/workflows/ci.yml
name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: npm ci
      - name: Build shard ${{ matrix.shard }}
        run: |
          PROJECTS=$(npx nx print-affected -t build --select=projects --base=origin/main)
          # 按 shard 分片执行(简化示意)
          npx nx run-many -t build --projects=$(echo $PROJECTS | python3 -c "
          import sys; projects = sys.stdin.read().strip().split(',');
          shard = ${{ matrix.shard }};
          selected = projects[shard-1::3];
          print(','.join(selected))
          ")

⚠️ 警告: 手动分片方案没有智能负载均衡——如果某个 shard 分到了耗时最长的项目,它会成为瓶颈。Nx Cloud 的优势就在于它会根据历史执行时间动态分配任务。

🔄 四、从 Turborepo 迁移到 Nx

4.1 两者的核心差异

特性 Nx Turborepo
项目图构建 深度源码分析 + AST 仅 package.json dependencies
受影响检测 ✅ 内置,基于 Git diff ✅ 内置,基于 Git diff
本地缓存 ✅ 基于内容哈希 ✅ 基于内容哈希
远程缓存 ✅ Nx Cloud / 自托管 ✅ Vercel Remote Cache / 自托管
分布式 CI ✅ Nx Cloud DTE ❌ 不支持
架构约束 ✅ Tag + ESLint 规则 ❌ 不支持
代码生成器 ✅ 内置 Generator ❌ 不支持
插件生态 ✅ 丰富(Vite、Next.js、Storybook 等) ⚠️ 有限
学习曲线 中等偏高
配置复杂度 中等

4.2 迁移步骤

# 第一步:在现有 Turborepo 项目中初始化 Nx
# Nx 可以和 Turborepo 共存,逐步迁移
npx nx init

# 第二步:为每个 package 创建 project.json
# Nx 需要 project.json 来定义 targets(对应 turbo.json 的 tasks)
# 可以用 generator 自动生成
npx nx generate @nx/workspace:project-json --project=utils

# 第三步:将 turbo.json 的 pipeline 转换为 nx.json 的 targetDefaults
// turbo.json — 迁移前
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    }
  }
}
// nx.json — 迁移后
{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["{projectRoot}/dist"],
      "cache": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": [],
      "cache": true
    },
    "lint": {
      "outputs": [],
      "cache": true
    }
  }
}
# 第四步:验证构建结果一致
# 先用 Turborepo 构建一次,记录输出
turbo run build --force
find . -path '*/dist/*.js' -exec md5sum {} \; > turbo-hashes.txt

# 再用 Nx 构建一次,对比输出
npx nx run-many -t build --skip-nx-cache
find . -path '*/dist/*.js' -exec md5sum {} \; > nx-hashes.txt

diff turbo-hashes.txt nx-hashes.txt  # 应该没有差异

# 第五步:删除 turbo.json,完成迁移
rm turbo.json
npm uninstall turbo

💡 提示: 迁移不需要一步到位。Nx 支持和 Turborepo 共存——你可以在同一个仓库中同时使用两个工具,逐步将任务从 Turborepo 迁移到 Nx。等所有团队成员都熟悉 Nx 后,再删除 Turborepo 配置。

💡 五、最佳实践与避坑指南

✅ 推荐做法

// libs/utils/project.json — 使用 production namedInput 排除测试文件
{
  "targets": {
    "build": {
      "inputs": ["production", "^production"],
      "outputs": ["{projectRoot}/dist"]
    }
  }
}
# ✅ 使用 useDaemonProcess 加速项目图计算
# nx.json 中设置 "useDaemonProcess": true
# Nx 会启动一个后台守护进程,避免每次命令都重新计算项目图

# ✅ 在 CI 中使用 --base 和 --head 明确指定比较范围
npx nx affected -t build --base=origin/main --head=HEAD

# ✅ 为不同类型的项目设置不同的缓存策略
# 构建产物缓存 7 天,测试结果缓存 3 天

❌ 避免做法

# ❌ 不要在 CI 中用 --skip-nx-cache(除非调试)
# 这会跳过所有缓存,每次都全量构建

# ❌ 不要把 .nx/cache 目录提交到 Git
echo ".nx/cache" >> .gitignore

# ❌ 不要在 serve/dev 任务上启用缓存
# 这些是长期运行的任务,缓存没有意义

# ❌ 不要忽略 "dependsOn" 配置
# 如果 api-client 依赖 utils 的构建产物,
# 必须在 api-client 的 build target 中声明 dependsOn: ["^build"]

⚠️ 关键注意事项

陷阱 症状 解决方案
shallow clone nx affected 计算出错误的变更范围 CI 中设置 fetch-depth: 0
环境变量未纳入缓存 不同环境的构建结果混用 配置 runtimeInputs
循环依赖 项目图构建失败,任务无限等待 nx graph 可视化检测并修复
缓存膨胀 .nx/cache 目录增长到几十 GB 配置 cacheDirectory 到独立磁盘,定期清理
分布式任务序列化问题 DTE 下某些任务需要特定执行顺序 dependsOn + inputs 正确声明依赖

📊 六、实战效果数据

在一个真实的中大型 Monorepo 中(15 个应用、80 个库、2800 个 TypeScript 文件)的测试数据:

指标 无优化 Nx affected Nx + 本地缓存 Nx + 远程缓存 + DTE
CI 构建时间 42 分钟 18 分钟 11 分钟 6 分钟
开发者本机构建 4 分 12 秒 1 分 30 秒 18 秒 18 秒
CI 计算资源消耗 100% 45% 28% 15%
月度 CI 费用 $2,400 $1,080 $672 $360

关键结论: Nx + 远程缓存 + 分布式构建的组合,可以将 CI 时间降低 85%、计算资源消耗降低 85%。对于月 CI 费用超过 $1000 的团队,引入 Nx Cloud 的 ROI 是正的。

🔧 相关工具推荐

  • Nx Cloud:官方远程缓存和分布式 CI 服务,免费层支持小团队
  • Nx Console:VS Code / JetBrains 插件,提供图形化的项目图查看和任务执行
  • Lerna:已被 Nx 团队接管,底层使用 Nx 的任务调度器
  • Turborepo:适合小型 Monorepo(<20 个项目),学习曲线更低
  • Moon:Rust 编写的 Monorepo 工具,性能优秀但生态较小

📝 总结

Nx 的三个核心机制——项目图、任务缓存、分布式构建——构成了一个完整的 Monorepo 性能优化体系。项目图让你只构建受影响的项目(节省 60-70%),本地缓存让你跳过未变更的任务(再节省 80%+),远程缓存和分布式 CI 让团队之间共享构建结果并并行执行(再节省 60%+)。

选型建议:如果你的 Monorepo 超过 10 个项目、CI 时间超过 10 分钟,Nx 值得投入。如果只有 3-5 个项目,Turborepo 的简洁性可能更适合。不要为了用 Nx 而用 Nx——工具的价值在于解决实际问题,而不是增加架构复杂度

📚 相关文章