最容易踩的坑
要点
- 前面几篇一直在讲 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 的性能问题,很多时候不是工具没配,而是边界先长歪了。回头检查 inputs、outputs 是否精确,以及包之间的依赖关系是否合理,通常比升级工具版本更有效。
4. 第三个坑:团队开始默认"反正都在一个仓库里"
这是一种很常见、但又很难一下子察觉到的变化。
仓库在一起以后,大家会很容易慢慢形成一种偷懒心态:
反正代码都在一个仓库里,先改了再说。
反正这个包谁都能引用,先 import 进来再说。
反正现在能跑,后面要不要整理以后再看。
monorepo 最怕的不是大家不会用工具,而是这种"反正都在一起"的松弛感。
因为一旦所有边界都靠"以后再收",仓库前面省下来的切换成本,很快就会在后面变成更大的维护成本。你会发现依赖关系越来越乱,有些包被半个仓库引用但谁也说不清它的职责,改动影响范围变得越来越难判断。
对策很朴素: 定期检查依赖图(turbo run build --dry 可以看到),如果某个包被太多消费方引用而自身边界不清晰,就是时候重新拆分了。
5. 第四个坑:lockfile 冲突变成日常
这是多人协作的 monorepo 里几乎一定会碰到的问题。
当多个人同时在不同分支上添加或升级依赖时,lockfile(yarn.lock、pnpm-lock.yaml)会频繁冲突。因为 lockfile 本质上是一个大型扁平文件,不同分支对它的修改很难自动合并。
这个问题在 monorepo 里比单仓库更频繁,因为仓库里有更多的 package.json,依赖变动的频率也更高。
缓解方式:
- 尽量集中处理依赖升级,不要让每个人各自零散地
yarn add - 冲突发生时,不要手动编辑 lockfile——删掉 lockfile 重新
yarn install(或pnpm install)让包管理器重新生成 - 考虑用
yarn dedupe或pnpm dedupe定期清理冗余依赖
6. 第五个坑:运行环境的边界悄悄变模糊
前面一篇已经专门讲过这个问题,但它值得在踩坑章节里再强调一次,因为它是一种"开始时不显眼,后面代价最大"的错误。
浏览器端的代码开始依赖 Node 才有的能力。只在 worker 里能跑的逻辑被顺手引入前端。某个共享包里混着浏览器 API 和 Node API,谁用了谁就可能在某个环境里炸。
这类问题不会立刻报错。TypeScript 不拦,构建不拦,只有运行时才暴露。而且暴露时往往已经很难追溯是哪次引入埋下的。
守住这条线的方法前面已经讲过——按运行环境拆分共享包,不要让 packages/shared 变成一个什么运行时 API 都混在里面的杂烩。
7. 真正稳的 monorepo,不是功能更多,而是边界守得住
走到这里,很多人会把 monorepo 的问题理解成"工具还不够多"。
其实大多数时候并不是。
真正稳的 monorepo,通常不是因为它有更多高级能力,而是因为它有几条边界一直守得住:
- 业务逻辑先留在业务里
- 不同运行环境的代码不混着抽
- 包之间没有循环依赖
- 任务边界和产物边界说得清楚
inputs、outputs、env配得准确
这些事情看起来没有哪一项特别花哨,但它们往往决定了这个仓库是一年后还越来越顺,还是半年后就开始变沉。
8. 总结
monorepo 最常见的坑,很少是某个工具字段写错了,而是仓库在长大过程中,某些边界慢慢变松了。
这一篇列出的五个坑——循环依赖、任务变慢、心态松弛、lockfile 冲突、运行环境混合——它们的共同特点是:一开始不会立刻爆炸,但时间一长,会让仓库越来越沉。
所以与其记住"有哪些坑要避开",不如记住一个更实用的判断标准:如果改一个地方越来越难说清影响范围,那通常是某条边界需要重新审视了。