Monorepo实战中我们如何用pnpm和Turborepo提升前端开发效率
优化前:卡得不行
去年底把公司三个前端项目(一个内部管理后台、一个客户侧 SaaS 前台、一个独立的 UI 组件库)合并进一个 Turborepo 仓库,本想着“一套工具链管到底”,结果 CI/CD 直接崩了。本地跑 turbo build 要 5.3s,CI 上直接飙到 48s —— 还是只改了一个组件库里的按钮颜色。更离谱的是,每次 git commit 触发 pre-commit 的 lint + typecheck,我泡杯咖啡回来还没跑完。
最烦的是 dev server 启动:改完一行代码,热更新要等 2.7s 才生效。不是“慢”,是“你动一下,它喘三口气”。团队里新人第一天就问:“这个 monorepo 是不是配错了?” —— 我没法回答,因为我也在怀疑。
找到瘼颈了!
先甩命令:turbo build --dry-run --graph,生成依赖图。打开 HTML 图一看,好家伙,@myorg/ui 被所有包当 peer dep 引入,但实际构建时又全被 copy 进每个 dist 里,光这个包就占了总构建时间的 62%。
再用 pnpm run build --report(加了 rollup-plugin-visualizer),发现每个子包都在重复打包 React、ReactDOM、lodash-es —— 明明我们用了 external 配置,但 Turborepo 的缓存层根本没认出来这些是共享依赖,导致 cache miss 率 91%。
最后用 turbo trace 查文件变更影响范围,发现改 packages/utils/src/index.ts,居然触发了 apps/admin 和 apps/portal 全量 rebuild —— 它俩压根没 import 过这个 utils!查了一下午,原来是 tsconfig.json 里 references 写错了,把整个 packages/* 全 include 进去了,Turborepo 就当“可能用得上”,全扫一遍。
优化后:流畅多了
试了几种方案:换 Nx?太重,学习成本高;砍掉 Turborepo 改用 pnpm workspaces 自建 pipeline?不现实,CI 脚本得重写。最后决定死磕 Turborepo,只动三处:
- 第一刀:干掉冗余打包 —— 所有子包的
rollup.config.js加上严格external,并用pnpm的peerDependenciesMeta显式声明哪些不该被打进去 - 第二刀:切掉错误依赖追踪 —— 每个
tsconfig.json的references改成只引用真正用到的包,比如apps/admin/tsconfig.json只写"../ui"和"../api-client",删掉那行万恶的"../utils" - 第三刀:强制缓存穿透 —— 在
turbo.json里给buildtask 加"inputs": ["src/**/*", "tsconfig.json", "package.json"],并手动排除node_modules和dist,避免 Turborepo 把锁文件变更也当成“需要重跑”
核心代码就这几行,改完立刻测:
// packages/ui/rollup.config.js
import { defineConfig } from 'rollup';
import typescript from '@rollup/plugin-typescript';
export default defineConfig({
external: [
'react',
'react-dom',
'lodash-es',
// 注意这里:显式列出所有 runtime 依赖,别靠 guess
],
plugins: [typescript()],
});
还有关键的 pnpm-workspace.yaml 配置:
# pnpm-workspace.yaml
packages:
- 'packages/**'
- 'apps/**'
peerDependencyRules:
ignore: # 这个配置救了命
- react
- react-dom
- lodash-es
以及 turbo.json 里这句:
{
"tasks": {
"build": {
"inputs": ["src/**/*", "tsconfig.json", "package.json"],
"outputs": ["dist/**"]
}
}
}
性能数据对比
改完跑三轮取平均(Mac M2 Pro,16GB,SSD):
- 本地构建(turbo build):5.3s → 0.82s(下降 84%)
- CI 构建(GitHub Actions, ubuntu-latest):48s → 6.4s(下降 87%)
- dev server 启动:3.1s → 0.95s
- 热更新响应:2.7s → 320ms(改一行 TS,控制台打印 HMR log 到浏览器刷新,肉眼几乎无感)
- pre-commit lint+typecheck:12.6s → 1.4s(靠
turbo run lint --filter=...+--no-cache精准过滤)
顺手跑了下缓存命中率:turbo build --stats 显示 cache hit 从 9% 升到 89%。Turborepo 终于开始“记住”东西了。
踩坑提醒:这三点一定注意
第一,tsconfig.json 的 references 别偷懒。我之前图省事写了个 "../packages/*",以为“反正都是 workspace 包”,结果 Turborepo 把没用到的包也纳入依赖图,一改就全 rebuild。现在每个包都手工维护 references,多花两分钟,省下两小时调试时间。
第二,external 不是写完就完事。必须和 peerDependencies 严格对齐。比如你 external: ['react'],但 package.json 里没写 "peerDependencies": {"react": "^18.2.0"},Turborepo 缓存就会失效 —— 它认为“你可能偷偷用了不同版本的 react”。实测过,漏一个 peer,cache hit 掉 30%。
第三,别信文档里说的“Turborepo 自动识别 workspace dependencies”。它只认 package.json 的 dependencies,不认 devDependencies 或 optionalDependencies。我们有个包把 typescript 放在 devDependencies,结果每次 tsc --build 都 miss cache。挪到 dependencies 就好了 —— 虽然语义上不合理,但能跑,就先这样。
以上是我的优化经验,有更好的方案欢迎交流
这套方案不是银弹。比如现在 pnpm build 依然比 turbo build 快 0.1s(因为 turbo 多了层调度开销),但我已经不纠结了 —— 毕竟 turbo 的 cache 跨机器同步、CI 分片能力,远不是 pnpm 单机命令能比的。
还有个小问题没彻底解决:如果同时改 ui 和 admin,第一次 turbo build 会卡顿 1s 左右,推测是 cache 初始化 IO。不过不影响日常开发,我把它归为“可接受的启动噪音”。
以上是我踩坑三个月后的实战总结。如果你也在用 Turborepo / Nx / Rush 被构建速度折磨,欢迎评论区甩出你的配置,一起扒日志。另外,下篇可能会聊 “monorepo 下如何做细粒度的 E2E 测试隔离”,因为上周刚被一个跨包的 Cypress 测试搞崩溃……

暂无评论