在线工具集

单仓 vs 多仓:Bazel / Nx / Turborepo / pnpm workspaces

monorepo 和 multirepo 之争经常被简化成"Google 这么干所以一定对"或者"小团队不需要这么复杂",但真实决策要看协作边界、构建工具、发布节奏、CI 预算等多重因素。本文系统梳理两种模型的工程权衡,对比 Bazel、Nx、Turborepo、Lerna、pnpm workspaces 五种主流工具,结合 Google、Meta、字节、Vercel 的真实实践,最后给出适合不同规模团队的决策框架与混合模型的落地建议。

一、定义与边界:什么算 monorepo

monorepo 不是"把所有代码塞进一个 git 仓库"。它的核心定义有三条:所有项目共享同一个 git 历史与 commit 时间线;存在一套统一的工具链管理依赖、构建、测试;任何跨项目改动可以通过单个 PR 原子完成。同时满足这三条才是真正意义上的 monorepo。

反过来说,multirepo(也叫 polyrepo)的核心特征是:每个项目独立仓库、独立 CI、独立发布周期、跨项目改动必须分别提交多个 PR 协调合并。两种模型的差异不在仓库数量,而在协作边界。一个仓库塞了 30 个互不相关的项目仍然不是真正的 monorepo,而是"杂仓"。

有些团队以为引入 lerna 或 yarn workspaces 就完成了 monorepo 改造,实际上只解决了依赖管理的一小部分。完整的 monorepo 还需要原子构建、affected 检测、共享 lint、共享 CI 配置、版本管理统一策略,少一项体验都会下降。

二、monorepo 的核心收益

第一是原子变更。当一个 API 接口要从 v1 升 v2,monorepo 中可以一个 PR 同时改 server 实现、client 调用、SDK 类型、文档示例,CI 一次性验证,main 上从来不会出现"server 已升级 client 还没升级"的中间态。multirepo 则需要严格按版本号协调,发版顺序错一次就生产事故。

第二是共享依赖与配置。所有项目共享一份 lock file,第三方依赖版本天然一致,安全升级一次到位。eslint、prettier、tsconfig、jest 配置统一在 root,新项目继承零成本。multirepo 中每个仓库都要单独维护一套配置,几个月后版本就开始漂移,A 仓库 React 18、B 仓库 React 17、C 仓库还在 16,重构升级时痛苦倍增。

第三是知识扩散。代码全部在一处,搜索接口实现、grep 调用方、追溯 git blame 都可以一次完成。新人 onboarding 只需要 clone 一个仓库就拿到整个产品线的代码。这种"全局视野"在 multirepo 中几乎不可能复现。

三、multirepo 的核心收益

第一是边界隔离。每个仓库有独立的权限、独立的 CI、独立的依赖图。一个项目搞坏了 main 不会影响其他项目;一个团队改动不会触发其他团队的 CI;外部贡献者只需要面对自己关心的子集,认知负担低。

第二是发布节奏自由。前端可以每天发,移动端可以两周发,SDK 可以季度发,互不干扰。monorepo 强制大家某种程度上对齐节奏,否则会陷入"我想发但 main 上有别人的半成品"的困境。这一点在跨多个产品线、多个发布周期的组织里尤其重要。

第三是工具链简单。multirepo 不需要 affected 检测、不需要远程缓存、不需要复杂的 codeowners、不需要分布式构建。每个仓库一套传统 CI 配置,新人就能维护。这是 multirepo 在中小团队中能长期保持竞争力的根本原因。

四、Bazel:Google 级别的极致工程

Bazel 是 Google 内部 Blaze 的开源版本,主打"跨语言、超大规模、增量构建、远程缓存与远程执行"。它的 BUILD 文件用一种声明式 DSL 描述每个目标的依赖图,任何改动都能精确算出哪些目标需要重建。

Bazel 的最大优势在大规模多语言场景。Google、Stripe、字节、Pinterest 的核心仓库都用 Bazel,因为他们的代码同时包含 Java、C加加、Go、Python、JavaScript,传统每语言一套构建工具完全无法协同。Bazel 的 hermetic build(封闭构建)保证同样的输入永远产出同样的输出,缓存命中率极高。

但 Bazel 的代价也很大。BUILD 文件需要手动维护,依赖一旦写错排查很难;学习曲线极陡,团队需要一名专门的 build engineer;与社区生态(npm、pip、Maven)集成需要额外的 ruleset;增量收益要在仓库到达千万行级别才显著。中小团队引入 Bazel 是过度工程,回报远低于投入。

五、Nx:前端为主的全栈方案

Nx 由前 Google 工程师创立的 Nrwl 团队开发,最初针对 Angular,现在覆盖 React、Vue、Next.js、Node、NestJS 等几乎所有 JS/TS 框架。它的核心能力是依赖图分析、affected 检测、远程缓存、code generator、可视化工具。

Nx 最大的卖点是"约定大于配置"。一条命令 nx generate 就能创建符合最佳实践的新模块,自动接入 lint、test、build。affected 命令会算出本次 commit 受影响的所有项目,CI 只跑这部分,节省大量时间。Nx Cloud 提供远程缓存与分布式 CI,能把全量 build 时间从 30 分钟降到 3 分钟。

Nx 的代价是相对重的开箱即用配置和较强的"Nx 思维"。一旦项目脱离 Nx 推荐结构,定制成本会变高。建议在 5 到 50 个 package 的中型 monorepo、且以前端为主的场景使用 Nx,能享受到生态红利又不至于被 Bazel 复杂度淹没。需要在仓库结构图与 README 中可视化依赖时可以借助Markdown 预览工具

六、Turborepo:Vercel 的轻量解法

Turborepo 由 Vercel 收购的 Jared Palmer 团队打造,定位是"零配置、极轻量、快"。它只解决一件事:基于依赖图的任务编排与缓存。Turborepo 不管包管理(交给 npm/yarn/pnpm)、不管版本发布(交给 changesets)、不管模板生成(团队自由定义),只负责跑命令时的依赖排序与结果缓存。

Turborepo 的最大优势是上手成本极低。一个 turbo.json 文件描述每个任务的依赖关系,turbo run build 自动按拓扑顺序执行,命中缓存的任务直接复用结果。Turborepo Remote Cache 让团队成员之间和 CI 共享缓存,二次构建几乎为 0 成本。

Turborepo 适合 5 到 30 个 package 的中型团队,特别是 Next.js / Vercel 生态用户。如果你已经在用 pnpm workspaces,加一个 Turborepo 几乎是一晚上的事。它的缺点是功能边界较窄,不像 Nx 那样提供 generator、不像 Bazel 那样支持多语言。但对大多数 Web 团队,这种"窄而精"反而是优点。

七、pnpm workspaces 与 Lerna:基础层与历史选项

pnpm workspaces 是当前 monorepo 包管理的事实标准。它通过硬链接和符号链接让多个 package 共享一份全局 store,磁盘占用比 npm/yarn 小 50% 以上,安装速度也显著更快。pnpm 与 Turborepo、Nx 都能配合,是几乎所有现代 JS monorepo 的依赖管理基础。

Lerna 是早期 monorepo 工具的代表,2017 年到 2020 年几乎是 React 生态的默认选择。它解决了 npm publish 多包发布的痛点,提供 lerna run、lerna version、lerna publish 等命令。但 Lerna 的任务编排与缓存能力远不如 Nx 和 Turborepo,目前的角色更像"版本发布工具",与 Nx 集成后由 Nrwl 维护。

对新项目,推荐组合是 pnpm workspaces 加 Turborepo 加 changesets:pnpm 管依赖、Turborepo 跑任务、changesets 管版本与 changelog。这套组合学习成本低、社区成熟、跑得快,覆盖 80% 团队的需求。需要管理多包版本号格式时可以使用正则测试工具校验 SemVer 字符串。

八、Google、Meta、字节的实际做法

Google 是 monorepo 最坚定的实践者。整个公司主要代码都在一个内部仓库(代号 google3),超过 20 亿行代码、25000 名活跃开发者、每天 4 万次提交。构建工具是 Bazel 的内部版本 Blaze,依赖远程执行集群(Forge)和分布式缓存。Google 内部几乎不存在 multirepo 这个概念,新项目默认在主仓库下开一个目录。

Meta 走类似路线,但用自研工具 Buck(与 Bazel 类似)和 Sapling(替代 git 的源码控制系统)。Meta 的 Facebook、Instagram、WhatsApp 后端代码都在同一个仓库,前端代码在另一个超大 monorepo。原子重构是 Meta 工程文化的核心,例如把整个数据库迁移从 MySQL 切到 RocksDB 是单个跨产品线的协调工程。

字节内部的飞书、抖音、TikTok 各自有自己的核心 monorepo,仓库内部用 Bazel 或自研工具。这种"产品线 monorepo + 公司多 repo"的模式是国内大厂的常见形态,既保留产品内部的原子重构能力,又避免不同产品线相互干扰。中小团队可以参考这一思路,但不必照搬到这么大规模。

九、决策框架与混合模型

给一个简单决策树。3 人以下且 1 到 2 个项目:单仓单 package,连 monorepo 工具都不需要;3 到 10 人且 2 到 5 个相关项目:pnpm workspaces 加 Turborepo 的 monorepo;10 到 50 人且 5 到 20 个项目,前端为主:Nx monorepo;50 人以上、多语言、跨产品线:考虑 Bazel monorepo 或多个 monorepo 的混合模型。

混合模型是大公司常见的现实选择。核心业务放在一个或几个 monorepo(按产品线划分),边缘工具、开源项目、第三方 SDK 放在各自的 multirepo。划分原则是"经常一起改的代码放在一起"。如果两个项目几乎从不互相调用、发布节奏完全独立、团队也不重叠,硬塞进同一个 monorepo 反而是负担。

从 multirepo 迁移到 monorepo 不是一夜之间的事。推荐分阶段:先选 2 到 3 个高频联动的项目合并到 monorepo,跑 3 个月看效果;逐步把其他项目并入,每次合并都要重新评估 CI 时间、build 时间、开发者体验;保留某些独立性强的项目在 multirepo 中,不必追求完全统一。需要批量重命名或重组目录结构时可以使用文本 Diff 工具对比迁移前后差异。

常见问题

小团队应该选 monorepo 还是 multirepo?

十人以下、3 个以下相关项目的团队,monorepo 几乎总是更好的选择,因为可以原子重构、共享配置、统一 CI、避免版本碎片。当项目数超过 5 个或团队跨多个独立产品线,再考虑拆 multirepo 或采用混合模型。早期 multirepo 的最大代价是跨仓修改一致性变更需要协调多次发版,团队规模越小越承受不起这种摩擦。

pnpm workspaces 和 Turborepo 是替代关系吗?

不是。pnpm workspaces 是包管理与依赖链路工具,解决"多个 package 共享一份 node_modules"。Turborepo 是任务编排与缓存工具,解决"build/test 怎么按依赖图增量执行"。两者通常一起使用:pnpm 管依赖,Turborepo 跑任务,配合 changesets 做版本发布。Nx 同时覆盖这两层并提供更多框架插件。

Bazel 是不是必须用?

只有在多语言、超大规模、对增量构建有极致要求时才需要 Bazel。Google、Stripe、字节的核心仓库使用 Bazel,因为单次构建可能涉及上亿行代码,没有 Bazel 的缓存与远程执行体系根本不可行。前端为主或纯 Node 生态的团队用 Bazel 性价比很低,Turborepo 或 Nx 已经够用。Bazel 的学习曲线和维护成本都极高,不要轻易引入。

monorepo 怎么解决 CI 跑得太慢?

核心是 affected detection:每个 commit 只跑被改动模块及其下游的测试与构建。Nx、Turborepo、Bazel 都内建这种能力,会根据依赖图算出受影响范围。配合远程缓存(Turborepo Remote Cache、Nx Cloud、Bazel Remote Build Execution),公共依赖只构建一次全团队复用。CI 时间可以从全量 30 分钟降到平均 2 到 5 分钟。

混合模型怎么落地?

常见做法是核心业务放一个 monorepo(前端、后端、共享 SDK),边缘工具与独立产品线放各自的 multirepo。例如字节的飞书核心放在一个 monorepo,但开源项目、第三方 SDK、独立的小工具仍是单独仓库。这样既享受 monorepo 的原子重构与依赖一致性,又保留 multirepo 的隔离性。建议在拆分前先画清楚"哪些代码会一起变",让 git 边界匹配实际协作边界。

相关工具