pnpm、npm 与 Yarn Workspace 对比:为什么 Monorepo 更适合 pnpm?

pnpm、npm 与 Yarn Workspace 对比:为什么 Monorepo 更适合 pnpm?

在包含多个应用和公共包的 Monorepo 中,包管理器不仅负责“安装依赖”,还会影响依赖边界、磁盘占用、本地联调、批量执行和 CI 构建方式。

本文从实际工程角度比较 pnpm、npm 和 Yarn 的 Workspace 能力,并解释为什么 pnpm 经常成为中大型前端 Monorepo 的选择。

需要先说明:pnpm 并不是在所有方面都绝对优于 npm 或 Yarn。三者目前都具备 Workspace 能力。pnpm 的主要优势来自内容寻址存储、相对严格的依赖隔离,以及适合多包仓库的 workspace 协议和过滤执行能力。

一、Workspace 解决什么问题?

假设一个佣金系统同时包含四个应用:

1
2
3
4
5
6
7
8
9
10
11
12
monorepo/
├── apps/
│ ├── crm-pc/ # 销售 PC
│ ├── crm-mobile/ # 销售移动端
│ ├── ecom-pc/ # 商家 PC
│ └── ecom-mobile/ # 商家移动端
├── packages/
│ ├── share-root/ # 跨系统、跨终端共享
│ ├── share-crm/ # 销售系统内部共享
│ └── share-ecom/ # 商家系统内部共享
├── package.json
└── pnpm-workspace.yaml

四个应用需要独立启动、构建和上线,但又要复用请求封装、API、类型、通用组件、业务组件和公共样式。

Workspace 的核心价值是:

  • 在一个仓库中管理多个应用和包;
  • 让应用直接引用仓库内的公共包;
  • 统一安装和锁定依赖版本;
  • 对多个子包批量或按范围执行命令;
  • 降低跨仓库同步、临时发包和重复实现的成本。

二、传统 npm/Yarn Classic 的依赖提升

传统 npm 和 Yarn Classic 通常会尽可能将依赖提升到仓库根目录的 node_modules

1
2
3
4
5
6
7
monorepo/
├── node_modules/
│ ├── react/
│ ├── axios/
│ └── lodash/
├── apps/
└── packages/

依赖提升可以减少重复安装,也让 Node.js 的模块查找更加方便,但它可能掩盖“幽灵依赖”问题。

例如,crm-pc 没有在自己的 package.json 中声明 lodash,但其他子包安装了它,导致根目录存在 lodash

1
2
// crm-pc 实际没有声明 lodash,但本地可能仍能运行
import lodash from 'lodash';

这种代码可能在当前仓库中正常运行,但在以下场景中失败:

  • 子包被单独发布或迁移;
  • 依赖安装顺序发生变化;
  • 包管理器升级或提升结果变化;
  • CI 和本地安装结构不一致。

现代 npm 和 Yarn 已经持续改善 Workspace 和依赖管理能力,因此不能简单地把该问题归结为“npm/Yarn 一定存在,pnpm 一定不存在”。它更多反映不同包管理器默认依赖布局和约束强度的差异。

三、pnpm 如何存储和链接依赖?

pnpm 使用内容寻址存储保存依赖。相同内容通常只需要在磁盘中保存一份,多个项目再通过硬链接或符号链接复用。

概念上可以理解为:

1
2
3
4
5
全局内容存储
└── react@某版本
├── 项目 A 的链接
├── 项目 B 的链接
└── 项目 C 的链接

项目仍然具有 node_modules,但其中的包会通过链接关联到 .pnpm 结构及内容存储,而不是简单地为每个项目复制完整依赖。

这使 pnpm 在多个应用使用相同版本依赖时,通常具有更好的磁盘利用率和重复安装效率。

四、pnpm Workspace 的主要优势

1. 更高的磁盘利用率和安装效率

在 Monorepo 中,多个应用经常使用相同版本的 React、TypeScript、Axios 等依赖。pnpm 的内容寻址存储能够减少重复下载和文件复制。

项目越多、依赖重合度越高,这种存储方式越有价值。

不过,不应把结论表述为“pnpm 永远比 npm 和 Yarn 快”。安装速度还会受到以下因素影响:

  • 是否命中本地缓存;
  • 网络和 Registry 状态;
  • 生命周期脚本数量;
  • 锁文件是否发生变化;
  • 文件系统和 CI 缓存策略。

更准确的说法是:

在依赖重合度较高的多应用 Monorepo 中,pnpm 的内容寻址存储通常能够减少重复下载和文件复制,改善安装效率和磁盘利用率。

2. 相对严格的依赖隔离

pnpm 的 node_modules 结构更容易限制子包访问没有明确声明的依赖。

例如:

1
2
3
4
5
6
{
"name": "crm-pc",
"dependencies": {
"react": "^18.0.0"
}
}

如果 crm-pc 使用了 lodash,就应该在自身 package.json 中声明,而不能依赖其他子包“顺便”安装的版本。

更明确的依赖边界可以带来:

  • 子包独立构建时更加可靠;
  • 依赖关系更容易追踪;
  • 减少本地正常、CI 或发布后失败的问题;
  • 提高公共包迁移和复用的可靠性。

需要注意,Yarn Berry 的 Plug’n’Play 也可以提供严格的依赖约束,甚至比传统 node_modules 更严格。因此,合理表述是:

pnpm 在保留 node_modules 使用方式的同时,提供了相对严格的依赖隔离。

3. 使用 workspace: 协议明确引用本地包

应用可以通过 workspace: 协议引用仓库内的公共包:

1
2
3
4
5
6
{
"dependencies": {
"@commission/share-root": "workspace:*",
"@commission/share-crm": "workspace:*"
}
}

workspace:* 明确表达:该依赖必须解析为当前 Workspace 内的本地包,而不是从 Registry 下载同名版本。

它可以:

  • 避免意外引用远程同名包;
  • 修改共享包后直接在应用中联调;
  • 清晰展示本地包之间的依赖关系;
  • 减少跨包联调时发布临时版本的成本;
  • 在发布阶段按照规则转换为正式版本范围。

npm 和 Yarn 现在同样具有 Workspace 和本地包链接能力。pnpm 的优势不是“独家支持”,而是 workspace: 协议与其多包依赖管理体验结合得较好。

4. 适合多包仓库的过滤执行

pnpm 可以递归执行 Workspace 命令:

1
pnpm -r build

也可以只对某个应用执行:

1
pnpm --filter crm-pc build

过滤能力适合以下场景:

  • 四个应用独立启动和构建;
  • 本地只启动当前开发的应用;
  • CI 按目标应用执行任务;
  • 缩小测试、Lint 和排错范围;
  • 对目标包及其依赖执行命令。

但 pnpm 的过滤执行不等于完整的“变更影响分析”。如果需要根据 Git 变更自动计算复杂的受影响项目范围,通常还需要 Turborepo、Nx 或自研工具配合。

5. 统一锁文件和依赖治理

pnpm Workspace 通常使用统一的 pnpm-lock.yaml,可以带来:

  • 所有应用使用一致的依赖解析结果;
  • CI 安装具有更好的确定性;
  • 公共依赖升级更容易统一评估;
  • Code Review 可以集中查看依赖变化;
  • 减少多个仓库分别维护锁文件的成本。

统一锁文件也有代价:

  • 锁文件可能较大;
  • 单个包升级依赖也可能修改统一锁文件;
  • 多分支同时升级依赖时可能产生冲突。

这是统一治理收益与变更耦合成本之间的取舍。

五、pnpm 与 npm Workspace 对比

现代 npm 已支持 Workspace,因此 npm 完全可以管理普通的多包项目。

对比项 pnpm npm Workspace
依赖存储 内容寻址存储,通过链接复用 传统 node_modules 安装与提升为主
磁盘利用率 依赖高度重合时通常更优 简单项目中足够,可能存在更多重复文件
依赖隔离 默认结构相对严格 更偏向传统提升模型
本地包引用 workspace: 协议表达清晰 支持 Workspace 和本地链接
多包过滤执行 --filter 和递归命令较成熟 具备 Workspace 命令能力,复杂过滤体验有所不同
使用成本 需要团队安装和统一 pnpm 版本 Node.js 默认附带,上手成本低

npm 的主要优势包括:

  • Node.js 默认附带;
  • 团队学习和安装成本低;
  • 普通项目配置简单;
  • Workspace 能力已经可以满足许多中小项目。

因此,选择 pnpm 的理由不应是“npm 不能做 Monorepo”,而应该是:

pnpm 在多应用、共享包、依赖复用、依赖边界和过滤执行方面更符合当前项目需求。

六、pnpm 与 Yarn Workspace 对比

讨论 Yarn 时,需要区分 Yarn Classic 和 Yarn Berry。

Yarn Classic

Yarn Classic 主要采用传统 node_modules 和依赖提升模式。相较于 Yarn Classic,pnpm 通常具有:

  • 更高效的内容寻址存储;
  • 相对更严格的依赖边界;
  • 较清晰的 Workspace 协议;
  • 更适合多包范围执行的过滤能力。

Yarn Berry

Yarn Berry 的能力同样很强,包括:

  • Workspace;
  • Plug’n’Play;
  • Constraints;
  • 插件体系;
  • 面向大型 Monorepo 的依赖治理能力。

Plug’n’Play 不依赖传统 node_modules,可以提供非常严格的依赖管理和快速解析。但某些依赖 Node.js 传统模块结构的工具可能需要额外适配。

pnpm 则在保留 node_modules 使用习惯的同时,利用链接和内容寻址存储改善依赖管理。因此,对依赖传统 Node.js 工具链的团队来说,迁移和理解成本通常更可控。

对比项 pnpm Yarn Classic Yarn Berry/PnP
依赖结构 链接化 node_modules 传统提升式 node_modules 可使用 PnP,不依赖传统 node_modules
依赖边界 相对严格 较容易受提升影响 非常严格
工具兼容方式 接近传统 Node.js 生态 传统工具兼容性好 部分工具可能需要 PnP 适配
存储效率 内容寻址存储 传统缓存与安装 具有自身缓存和零安装等能力
多包治理 Workspace、filter Workspace Workspace、Constraints、插件体系

不能简单得出“pnpm 全面优于 Yarn Berry”的结论。对于需要高级约束、零安装或已深度使用 Yarn 插件体系的团队,Yarn Berry 可能更合适。

七、React Native 项目的注意事项

React Native 的 Metro 历史上对 Monorepo、符号链接和模块解析有额外要求。

使用 pnpm 时,React Native 项目可能需要关注:

  • Metro 是否正确监听 Workspace 中的共享包;
  • 是否配置 watchFolders
  • React 和 React Native 是否出现重复实例;
  • Metro 是否能够解析 pnpm 的链接化依赖结构;
  • .ios.tsx.android.tsx 等平台文件的解析;
  • 公共包是否需要预编译;
  • Native 依赖是否允许被提升或需要特殊配置。

因此,不应笼统声称“pnpm 对 React Native 的兼容性一定优于 Yarn”。更准确的结论是:

pnpm 适合多包依赖治理,但 React Native 子包仍需要结合 Metro 和 Native 依赖进行实际验证与配置。

八、不同技术栈为什么难以统一类型检查?

一个 Monorepo 可能同时包含:

  • JavaScript;
  • TypeScript;
  • React;
  • React Native。

这些子包的检查条件不同。

JavaScript 子包

JavaScript 包可能完全不执行 TypeScript 检查,也可能使用:

1
2
3
4
5
6
{
"compilerOptions": {
"allowJs": true,
"checkJs": true
}
}

普通 TypeScript 子包

普通 TS 工具包主要关注:

  • TypeScript 类型;
  • 模块解析;
  • 声明文件;
  • 编译目标;
  • 是否生成 declaration。

它不一定需要 JSX 或浏览器 DOM 类型。

React 子包

React Web 应用通常需要处理:

  • .tsx
  • JSX 编译模式;
  • React 类型;
  • DOM 类型;
  • Vite 或其他 Web 构建环境声明。
1
2
3
4
5
6
{
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ESNext", "DOM"]
}
}

React Native 子包

React Native 同样使用 React 和 JSX,但它不是浏览器环境:

  • 不应默认依赖完整 DOM;
  • 使用 React Native 平台类型;
  • 可能包含 .ios.tsx.android.tsx
  • Metro 的模块解析与 Web 构建工具不同;
  • Native 模块可能带有额外类型约束。

如果在根目录使用一份统一的 tsconfig 检查全部子包,可能出现:

  1. 为了兼容所有技术栈而放宽配置,导致检查不准确;
  2. 强制使用同一配置,产生大量与当前运行环境无关的报错;
  3. 每次检查都扫描整个仓库,随着规模增长造成性能问题。

更合理的方式是:

1
2
3
4
5
6
7
识别子包技术栈

读取该子包对应的 tsconfig

按配置和依赖关系组织待检查文件

执行适合该技术栈的类型检查

pnpm Workspace 解决的是多包依赖管理和本地协作问题,不会自动解决不同技术栈的类型检查问题。后者需要结合 TypeScript Compiler API、项目引用、构建工具或独立检查工具处理。

九、项目选型时可以如何表达?

下面是一段相对完整且不过度夸大的项目表述:

我们比较了 npm、Yarn 和 pnpm,最终选择 pnpm workspace。项目中包含四个应用和多个共享包,存在大量相同依赖,pnpm 的内容寻址存储能够减少重复下载和磁盘占用;相对严格的依赖隔离也能帮助我们发现子包使用了但没有在自身 package.json 中声明的幽灵依赖,使应用独立构建时更加可靠。同时,我们通过 workspace: 协议引用仓库内的共享包,让公共能力修改后可以直接在多个应用中联调,并通过 filter 能力按应用执行启动和构建。综合依赖治理、安装效率和多包开发体验,pnpm 更符合这个项目的需求。

如果项目实际没有使用某项能力,例如 --filter、严格依赖治理或 React Native 的 Metro 配置,就不应把它描述为已经落地的成果。技术选型的可信度来自真实场景和实际取舍,而不是罗列工具的全部特性。

十、总结

pnpm 适合 Monorepo 的核心原因可以概括为:

  1. 通过内容寻址存储减少重复依赖;
  2. 通过相对严格的依赖结构改善依赖边界;
  3. 通过 workspace: 协议支持本地公共包联调;
  4. 通过递归和 filter 能力按范围执行任务;
  5. 在保留 node_modules 使用方式的同时,提供较成熟的多包管理体验。

但包管理器选型仍应结合以下因素:

  • 团队已有工具链;
  • Node.js 和包管理器版本;
  • CI 缓存方案;
  • React Native、Metro 等特殊工具;
  • 是否需要 Yarn Berry 的 PnP、Constraints 或插件体系;
  • 仓库规模和依赖重合度。

最合理的选择不是“功能最多的包管理器”,而是最符合团队工程约束、并且能够长期稳定维护的方案。


pnpm、npm 与 Yarn Workspace 对比:为什么 Monorepo 更适合 pnpm?
http://example.com/2026/08/06/pnpm-npm-yarn-workspace对比/
Author
John Doe
Posted on
August 6, 2026
Licensed under