Monorepo实战中我们如何用pnpm和Turborepo提升前端开发效率

技术景鑫 前端 阅读 1,047
赞 36 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

去年底把公司三个前端项目(一个内部管理后台、一个客户侧 SaaS 前台、一个独立的 UI 组件库)合并进一个 Turborepo 仓库,本想着“一套工具链管到底”,结果 CI/CD 直接崩了。本地跑 turbo build 要 5.3s,CI 上直接飙到 48s —— 还是只改了一个组件库里的按钮颜色。更离谱的是,每次 git commit 触发 pre-commit 的 lint + typecheck,我泡杯咖啡回来还没跑完。

Monorepo实战中我们如何用pnpm和Turborepo提升前端开发效率

最烦的是 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/adminapps/portal 全量 rebuild —— 它俩压根没 import 过这个 utils!查了一下午,原来是 tsconfig.jsonreferences 写错了,把整个 packages/* 全 include 进去了,Turborepo 就当“可能用得上”,全扫一遍。

优化后:流畅多了

试了几种方案:换 Nx?太重,学习成本高;砍掉 Turborepo 改用 pnpm workspaces 自建 pipeline?不现实,CI 脚本得重写。最后决定死磕 Turborepo,只动三处:

  • 第一刀:干掉冗余打包 —— 所有子包的 rollup.config.js 加上严格 external,并用 pnpmpeerDependenciesMeta 显式声明哪些不该被打进去
  • 第二刀:切掉错误依赖追踪 —— 每个 tsconfig.jsonreferences 改成只引用真正用到的包,比如 apps/admin/tsconfig.json 只写 "../ui""../api-client",删掉那行万恶的 "../utils"
  • 第三刀:强制缓存穿透 —— 在 turbo.json 里给 build task 加 "inputs": ["src/**/*", "tsconfig.json", "package.json"],并手动排除 node_modulesdist,避免 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.jsonreferences 别偷懒。我之前图省事写了个 "../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.jsondependencies,不认 devDependenciesoptionalDependencies。我们有个包把 typescript 放在 devDependencies,结果每次 tsc --build 都 miss cache。挪到 dependencies 就好了 —— 虽然语义上不合理,但能跑,就先这样。

以上是我的优化经验,有更好的方案欢迎交流

这套方案不是银弹。比如现在 pnpm build 依然比 turbo build 快 0.1s(因为 turbo 多了层调度开销),但我已经不纠结了 —— 毕竟 turbo 的 cache 跨机器同步、CI 分片能力,远不是 pnpm 单机命令能比的。

还有个小问题没彻底解决:如果同时改 uiadmin,第一次 turbo build 会卡顿 1s 左右,推测是 cache 初始化 IO。不过不影响日常开发,我把它归为“可接受的启动噪音”。

以上是我踩坑三个月后的实战总结。如果你也在用 Turborepo / Nx / Rush 被构建速度折磨,欢迎评论区甩出你的配置,一起扒日志。另外,下篇可能会聊 “monorepo 下如何做细粒度的 E2E 测试隔离”,因为上周刚被一个跨包的 Cypress 测试搞崩溃……

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论