最容易踩的坑

要点

  • 前面几篇一直在讲 monorepo 是怎么一步一步变顺手的
  • 这是一种很容易在 monorepo 里不知不觉长出来的问题
  • 很多人上 monorepo 以后,会先明显感受到入口统一、任务更顺
  • 这是一种很常见、但又很难一下子察觉到的变化
  • 这是多人协作的 monorepo 里几乎一定会碰到的问题

内容

1. monorepo 很少在第一天就坏掉,它更常见的走法是慢慢长歪

前面几篇一直在讲 monorepo 是怎么一步一步变顺手的。

目录摆好了,本地包能互相引用了,任务有统一入口了,缓存也开始工作了。

但真正麻烦的地方,往往不是一开始,而是后面。

monorepo 最常见的坑不是某个配置写错了——那种问题一跑就知道。更难察觉的是那些慢慢长出来的结构性问题,等到你意识到时,仓库已经开始变沉了。

共享边界怎么守、代码什么时候该提到 packages——这些问题前面的章节已经讲过。这一篇专门聚焦那些更隐蔽、更难察觉的坑。

2. 第一个坑:包之间出现了循环依赖

这是一种很容易在 monorepo 里不知不觉长出来的问题。

packages/shared 引用了 packages/ui 里的一个类型。

packages/ui 又引用了 packages/shared 里的一个工具函数。

从各自的角度看,每一条引用都是合理的。但两个包互相引用以后,问题就来了:

  • 构建顺序无法确定——Turborepo 的 dependsOn: ["^build"] 要求上游先构建完,但两个包互为上游,形成死锁
  • TypeScript 的类型解析可能出现不可预测的行为
  • 引用链越长越绕,后面改一个地方越来越难判断影响范围

怎么发现: Turborepo 在遇到循环依赖时通常会报错或警告。也可以用 turbo run build --dry 查看任务图,循环依赖会让图变得无法拓扑排序。

怎么处理: 通常意味着两个包之间的边界没有摆对。要么把互相引用的那部分代码提到第三个包里,要么其中一方的引用本不该存在。

3. 第二个坑:任务虽然统一了,但仓库还是在悄悄变慢

很多人上 monorepo 以后,会先明显感受到入口统一、任务更顺。但如果后面不继续收边界,仓库还是会慢慢变慢。

最常见的原因并不神秘:

  • 包拆得太碎。 改一个地方会牵动一串上游和下游任务。10 个包的构建因为一处小改动全部重跑。
  • inputs 一直没配或配得太宽。 改了文档、改了测试,build 的缓存也失效了。
  • 有些不该一起跑的任务,被默认绑到所有包上。 比如只有部分包有 test,但 turbo run test 还是会遍历所有包。

于是你会看到一种很别扭的状态:仓库看起来有 workspace、Turborepo 和缓存,可日常开发里还是越来越容易出现"动一点就感觉整仓都在响"的体感。

关键: monorepo 的性能问题,很多时候不是工具没配,而是边界先长歪了。回头检查 inputsoutputs 是否精确,以及包之间的依赖关系是否合理,通常比升级工具版本更有效。

4. 第三个坑:团队开始默认"反正都在一个仓库里"

这是一种很常见、但又很难一下子察觉到的变化。

仓库在一起以后,大家会很容易慢慢形成一种偷懒心态:

反正代码都在一个仓库里,先改了再说。

反正这个包谁都能引用,先 import 进来再说。

反正现在能跑,后面要不要整理以后再看。

monorepo 最怕的不是大家不会用工具,而是这种"反正都在一起"的松弛感。

因为一旦所有边界都靠"以后再收",仓库前面省下来的切换成本,很快就会在后面变成更大的维护成本。你会发现依赖关系越来越乱,有些包被半个仓库引用但谁也说不清它的职责,改动影响范围变得越来越难判断。

对策很朴素: 定期检查依赖图(turbo run build --dry 可以看到),如果某个包被太多消费方引用而自身边界不清晰,就是时候重新拆分了。

5. 第四个坑:lockfile 冲突变成日常

这是多人协作的 monorepo 里几乎一定会碰到的问题。

当多个人同时在不同分支上添加或升级依赖时,lockfile(yarn.lockpnpm-lock.yaml)会频繁冲突。因为 lockfile 本质上是一个大型扁平文件,不同分支对它的修改很难自动合并。

这个问题在 monorepo 里比单仓库更频繁,因为仓库里有更多的 package.json,依赖变动的频率也更高。

缓解方式:

  • 尽量集中处理依赖升级,不要让每个人各自零散地 yarn add
  • 冲突发生时,不要手动编辑 lockfile——删掉 lockfile 重新 yarn install(或 pnpm install)让包管理器重新生成
  • 考虑用 yarn dedupepnpm dedupe 定期清理冗余依赖

6. 第五个坑:运行环境的边界悄悄变模糊

前面一篇已经专门讲过这个问题,但它值得在踩坑章节里再强调一次,因为它是一种"开始时不显眼,后面代价最大"的错误。

浏览器端的代码开始依赖 Node 才有的能力。只在 worker 里能跑的逻辑被顺手引入前端。某个共享包里混着浏览器 API 和 Node API,谁用了谁就可能在某个环境里炸。

这类问题不会立刻报错。TypeScript 不拦,构建不拦,只有运行时才暴露。而且暴露时往往已经很难追溯是哪次引入埋下的。

守住这条线的方法前面已经讲过——按运行环境拆分共享包,不要让 packages/shared 变成一个什么运行时 API 都混在里面的杂烩。

7. 真正稳的 monorepo,不是功能更多,而是边界守得住

走到这里,很多人会把 monorepo 的问题理解成"工具还不够多"。

其实大多数时候并不是。

真正稳的 monorepo,通常不是因为它有更多高级能力,而是因为它有几条边界一直守得住:

  • 业务逻辑先留在业务里
  • 不同运行环境的代码不混着抽
  • 包之间没有循环依赖
  • 任务边界和产物边界说得清楚
  • inputsoutputsenv 配得准确

这些事情看起来没有哪一项特别花哨,但它们往往决定了这个仓库是一年后还越来越顺,还是半年后就开始变沉。

8. 总结

monorepo 最常见的坑,很少是某个工具字段写错了,而是仓库在长大过程中,某些边界慢慢变松了。

这一篇列出的五个坑——循环依赖、任务变慢、心态松弛、lockfile 冲突、运行环境混合——它们的共同特点是:一开始不会立刻爆炸,但时间一长,会让仓库越来越沉。

所以与其记住"有哪些坑要避开",不如记住一个更实用的判断标准:如果改一个地方越来越难说清影响范围,那通常是某条边界需要重新审视了。