lerna vs turbo
Monorepo 架构选型:Lerna 与 Turbo 的深度技术对比
lernaturbo类似的npm包:

Monorepo 架构选型:Lerna 与 Turbo 的深度技术对比

lernaturbo 都是用于管理 JavaScript 和 TypeScript 多包仓库(Monorepo)的核心工具,旨在解决代码共享、版本控制和构建效率问题。lerna 是这一领域的先驱,主要专注于多包项目的版本发布、变更检测以及依赖链接,它通过运行命令来协调各个包的生命周期。turbo(Turbo Repo)则是一个高性能的构建系统,它引入了基于缓存的任务管道(Pipeline),能够智能地跳过未受影响的任务,并在多核 CPU 上并行执行构建、测试和 lint 操作,极大地提升了大型项目的开发体验。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
lerna036,054608 kB29113 天前MIT
turbo031,04157.9 kB157 天前MIT

Lerna vs Turbo:Monorepo 架构的性能、工作流与选型指南

在大型前端工程中,Monorepo(多包仓库)已成为管理共享代码、统一工具链的标准模式。lernaturbo 是这一领域最知名的两个工具,但它们的侧重点截然不同。简单来说,lerna 更像是一个项目管理者,专注于包的版本和发布;而 turbo 更像是一个高性能引擎,专注于构建速度和任务调度。让我们深入对比它们在真实工程场景中的表现。

🚀 核心定位:版本发布 vs 构建管道

lerna 的核心使命是解决“多包如何一起版本化”的问题。

  • 它擅长检测哪些包发生了变化。
  • 它负责更新版本号、生成 CHANGELOG 并将包发布到 npm。
  • 它的传统强项在于运行命令(如 lerna run build),但其执行模型相对线性。
# lerna: 典型的发布工作流
# 自动检测变更,提示版本号,更新 package.json 并打标签
lerna version
# 将所有包发布到 npm
lerna publish

turbo 的核心使命是解决“构建太慢”的问题。

  • 它不关心版本号,只关心任务(Task)的执行效率。
  • 它通过哈希算法缓存任务结果,如果代码没变,直接复用缓存,秒级完成构建。
  • 它利用多核 CPU 并行运行任务,大幅缩短等待时间。
# turbo: 典型的构建工作流
# 自动识别依赖关系,并行构建,命中缓存的任务直接跳过
npx turbo run build
# 查看缓存命中情况和任务拓扑图
npx turbo run build --graph

⚡ 执行模式:线性遍历 vs 智能并行

lerna 在运行任务时,默认通常是按顺序或简单的并行处理。

  • 虽然支持 --parallel 标志,但它缺乏对任务间依赖关系的深度理解。
  • 如果包 A 依赖包 B,你需要手动确保顺序,或者接受潜在的竞争条件。
  • 在大型仓库中,这可能导致大量重复计算。
// lerna.json 配置示例
{
  "packages": ["packages/*"],
  "version": "independent",
  "command": {
    "run": {
      "parallel": true
    }
  }
}
# lerna 运行命令:尝试并行,但缺乏智能依赖排序
lerna run test --parallel

turbo 内置了强大的依赖感知引擎。

  • 它读取 turbo.json 中的管道定义,自动构建任务依赖图。
  • 它会先构建底层依赖包,再并行构建上层应用,确保顺序正确且速度最快。
  • 这种“拓扑排序”能力是其高性能的关键。
// turbo.json 配置示例
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    }
  }
}
# turbo 运行命令:自动根据依赖图并行执行
# 如果包 A 依赖包 B,turbo 会先构建 B,再构建 A
npx turbo run build

💾 缓存机制:无状态 vs 本地/远程缓存

lerna 本身没有内置缓存机制

  • 每次运行 lerna run build,它都会重新执行所有脚本。
  • 即使你只改了一行字,它也可能重新编译整个仓库(除非你在自己的 npm 脚本里做了缓存)。
  • 这在 CI 环境中会导致巨大的资源浪费和时间消耗。
# lerna: 每次都是全量执行
# 即使没有代码变更,也会重新跑一遍
lerna run build
# 输出:[包 A] 构建中... [包 B] 构建中...

turbo 拥有基于内容的缓存系统

  • 它会根据输入文件(代码、环境变量、依赖)的哈希值生成缓存键。
  • 如果哈希值匹配,它直接从本地 .turbo 文件夹或远程云端恢复输出结果。
  • 这使得二次构建几乎瞬间完成。
# turbo: 智能缓存
# 第一次运行:执行构建并缓存
npx turbo run build
# 第二次运行(无代码变更):
# 输出:• Packages in scope: ... 
#       • Running build in 2 packages
#       • Remote caching disabled
#       app-ui:build: cache hit, replaying output 8a3d... (0ms)

📦 依赖管理:链接 vs 传递

lerna 提供了经典的 lerna bootstrap 功能(虽在新版中逐渐被包管理器替代)。

  • 它通过创建符号链接(symlinks),将本地包链接到 node_modules
  • 这使得在开发时可以即时引用本地修改的代码,无需发布。
  • 现代实践中,常推荐使用 npm workspacespnpm workspaces 替代此功能,仅保留 lerna 的发布能力。
# lerna: 传统链接方式
# 将 packages 下的所有包链接到彼此和根目录的 node_modules
lerna bootstrap

# 现代推荐:结合 npm workspaces 使用
# package.json
{
  "workspaces": ["packages/*"]
}
# 然后仅用 lerna 做发布
lerna publish

turbo 不处理依赖链接

  • 它假设你已经通过 npmyarnpnpmbun 的 Workspaces 功能处理好了依赖。
  • 它专注于在这些依赖就位后,如何高效地运行脚本。
  • 这种单一职责原则让它的配置更简单,干扰更少。
// turbo 项目中,依赖管理完全交给包管理器
// package.json (根目录)
{
  "packageManager": "pnpm@8.0.0",
  "workspaces": ["apps/*", "packages/*"]
}
// turbo 只关心如何运行任务
// turbo.json
{
  "pipeline": { "build": {} }
}

🛠️ 配置复杂度:约定优于配置 vs 声明式管道

lerna 的配置相对分散。

  • 全局配置在 lerna.json,但具体命令行为往往依赖各个包的 package.json 脚本。
  • 对于复杂的发布策略(如预发布、canary 版本),配置项较多,学习曲线较陡。
  • 它更像是一个“胶水”工具,把各个包粘在一起。
// lerna.json: 复杂的发布配置
{
  "version": "independent",
  "changelog": {
    "preset": "angular"
  },
  "command": {
    "publish": {
      "registry": "https://my-private-registry.com"
    }
  }
}

turbo 采用声明式管道配置

  • 所有任务逻辑集中在 turbo.json 中,清晰定义输入、输出和依赖。
  • 这种配置方式让构建流程变得可预测、可文档化。
  • 对新加入的开发者非常友好,一眼就能看懂构建顺序。
// turbo.json: 清晰的管道定义
{
  "pipeline": {
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    }
  }
}

🤝 共同点:Monorepo 的基石

尽管侧重点不同,两者在宏观目标上是一致的:

1. 📂 支持多包结构

  • 都理解 packages/apps/ 这样的目录结构。
  • 都能识别 package.json 中的名称和依赖关系。
// 两者都识别这种标准结构
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"]
}

2. 🔄 脚本运行能力

  • 都允许你在根目录运行命令,并分发到子包。
  • 都支持过滤功能(如只运行特定包的命令)。
# lerna 过滤
lerna run test --scope=package-a

# turbo 过滤
npx turbo run test --filter=package-a

3. 🌐 生态系统兼容

  • 都与 React, Vue, Node.js 等主流技术栈完美兼容。
  • 都可以与 CI/CD 系统(GitHub Actions, GitLab CI)无缝集成。

📊 总结对比表

特性lernaturbo
核心强项🏷️ 版本管理、发布、变更日志⚡ 构建速度、缓存、并行管道
缓存机制❌ 无内置缓存✅ 本地 + 远程共享缓存
任务调度🔁 线性或简单并行🧠 智能依赖拓扑排序
依赖链接✅ 内置 (bootstrap) / 推荐配合 Workspaces❌ 依赖外部包管理器 (npm/pnpm)
配置风格📝 lerna.json + 分散脚本🗂️ 声明式 turbo.json 管道
最佳场景需要复杂版本策略的库开发团队追求极致 CI/CD 速度的应用团队

💡 架构师建议

选择 lerna 如果: 你正在维护一个组件库工具包集合,并且最头疼的问题是“如何同时发布 10 个包并保持版本号同步”。你需要它强大的 versionpublish 命令,以及生成变更日志的能力。在现代架构中,常见的做法是:使用 pnpm workspaces 管理依赖和链接,仅使用 lerna 管理发布流程。这种组合既利用了 pnpm 的速度,又保留了 lerna 的发布优势。

选择 turbo 如果: 你正在开发一个大型应用系统(包含多个前端应用、共享 UI 库、后端服务),并且最头疼的问题是“每次提交都要等 20 分钟构建”。你需要它的缓存机制来加速本地开发,需要它的并行管道来缩短 CI 时间。对于新项目,turbo + pnpm 是目前业界公认的高性能黄金组合。

终极方案:混合使用 很多成熟团队会同时使用两者(或类似组合):

  • turbo 处理日常的 build, test, lint(追求速度)。
  • lerna 处理最终的 version, publish(追求规范)。
// package.json 脚本示例
{
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test",
    "release": "lerna version && lerna publish"
  }
}

最后的话:工具没有绝对的优劣,只有适不适合。如果你的痛点是“慢”,选 turbo;如果你的痛点是“乱”(版本混乱),选 lerna。在现代前端工程中,理解它们的边界并将它们组合使用,往往能发挥最大的效能。

如何选择: lerna vs turbo

  • lerna:

    如果你的团队主要痛点在于版本管理发布流程,特别是需要处理复杂的独立版本(Independent Versioning)或自动生成变更日志,那么 lerna 仍然是首选。它非常适合那些已经拥有成熟构建脚本,仅需一个工具来协调包之间依赖链接和发布节奏的项目。注意:现代实践中,常将 lerna 仅用于发布管理,而配合其他工具处理构建。

  • turbo:

    如果你的项目面临构建速度慢CI 耗时过长的问题,或者你需要一个能够自动缓存任务结果、仅构建受代码变更影响部分的系统,那么 turbo 是更优的选择。它适合追求极致开发效率、希望利用远程缓存加速团队协作,且愿意采用基于 package.json 脚本声明式配置管道的新项目。

lerna的README

Lerna

Lerna is a fast, modern build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository.

NPM Status CI Status

Usage

Check out our docs site here.