npm vs yarn vs pnpm:包管理器怎么选
三个包管理器到底差在哪?从 node_modules 结构、锁文件、依赖解析、monorepo 支持、性能与坑点出发,小明给你一份可落地的选择指南:什么时候用 npm,什么时候用 yarn,什么时候该上 pnpm。
npm vs yarn vs pnpm:包管理器怎么选
你团队里一定出现过这种对话:
- A:“我们用 npm 就行,官方的最稳。”
- B:“yarn 更快啊,装依赖像开了挂。”
- C:“pnpm 才是未来,磁盘占用直接砍半。”
然后争论会迅速退化成宗教战争:
- “我一直用它,从没出过问题。”
- “别人都在用,肯定更好。”
- “你不用就是落后。”
但作为工程师,我们不靠信仰选工具,我们靠:约束、机制、权衡。
今天这篇文章我不想给你一个“标准答案”,而是给你一套选择框架:
- 你能解释它们差在哪
- 你能知道你的项目更需要什么
- 你能在 CI/团队协作/monorepo 下少踩坑
1. 先把结论放前面:怎么选最快?
如果你只想要一个“先用起来”的答案:
- 单仓库、小团队、追求稳定:选
npm - 需要 Yarn Berry(PnP)、强约束与现代工作流:选
yarn - monorepo / 依赖多 / 想要更快更省磁盘:选
pnpm
但别急着关页面。
因为你真正要做的是:在下面 5 个维度里,找到你项目的优先级:
- 安装性能(快不快)
- 磁盘占用(省不省)
- 依赖隔离(会不会“偷吃”依赖)
- 锁文件与可复现构建(CI 能不能稳定复现)
- monorepo 体验(workspace/hoist/版本策略)
2. 三者共同点:它们都在做同一件事
无论 npm/yarn/pnpm,核心流程都类似:
- 读
package.json - 解析依赖图(版本范围、peerDependencies、optionalDependencies…)
- 选择具体版本(resolution)
- 下载包(cache)
- 把包放进项目可用的位置(node_modules 或 PnP)
- 写锁文件,保证可复现
所以差异主要出现在两个地方:
- “依赖怎么落盘”(node_modules 怎么长)
- “依赖怎么解析/复用”(性能、隔离与坑)
3. 最大的分水岭:node_modules 的形状决定了你的工程体验
3.1 npm:默认扁平化(hoist),兼顾兼容性
npm 的 node_modules 通常比较“扁平”:
- 依赖会尽量被提升到顶层 node_modules
- 好处:减少重复包,路径更短,兼容老生态
- 代价:可能出现“幽灵依赖”(你没声明,但能 import 到)
所谓幽灵依赖就是:
+ 你的包 A 没写 dependencies: { lodash }
+ 但某个依赖 B 依赖 lodash
+ lodash 被 hoist 到顶层
→ A 也能 import 'lodash' 成功
这在本地能跑,但换个安装顺序/换台机器/锁文件变动,可能就炸。
3.2 yarn:两条路线(Classic vs Berry),Berry 甚至能不要 node_modules
你需要知道 Yarn 的“分裂史”:
- Yarn Classic(v1):和 npm 类似,也用 node_modules + hoist,主打速度
- Yarn Berry(v2+):最有特点的是 PnP(Plug'n'Play)
PnP 的核心是:
- 不把依赖展开成 node_modules 树
- 用一个映射文件(
.pnp.cjs)告诉 Node/工具:某个包应该从哪里加载
好处:
- 安装极快(少了大量文件操作)
- 依赖边界非常清晰(没声明就不能用)
代价:
- 一些工具链可能需要额外配置(不过近几年生态已经好很多)
- 你团队要统一认知,不然“为什么我找不到 node_modules”会吵一周
3.3 pnpm:硬链接 + 内容寻址存储,天然避免幽灵依赖
pnpm 的一句话卖点是:
同一份依赖,只存一份;项目里通过链接复用。
它的核心机制是 content-addressable store:
- 包会被存到一个全局 store(按内容 hash 存)
- 项目里的
node_modules不是直接复制包 - 而是通过硬链接/符号链接把包“挂”进来
这带来三个非常实在的结果:
- 安装快(大量复用)
- 磁盘省(不重复存)
- 依赖更严格(默认不随便 hoist,幽灵依赖更少)
pnpm 的 node_modules 看起来会更“奇怪”(有 .pnpm 目录),但它反而更接近真实依赖关系。
4. 锁文件:CI 稳不稳,基本靠它
三者都能锁定版本,但锁文件格式不同:
- npm:
package-lock.json - yarn:
yarn.lock - pnpm:
pnpm-lock.yaml
你需要关心的不是“长得像什么”,而是:
4.1 你是否能做到“可复现安装”?
推荐的工程实践(无论用谁):
- CI 上用“严格安装”
- npm:
npm ci - pnpm:
pnpm install --frozen-lockfile - yarn:
yarn install --immutable
- npm:
这能保证:
- 锁文件不一致就直接失败
- 防止有人本地安装把版本悄悄漂移
4.2 你是否锁住了 Node 版本与包管理器版本?
这是团队协作里最容易被忽略、但收益巨大的点。
建议:
- 用
engines声明 Node 版本 - 用 Corepack 锁包管理器
比如在 Node 20+:
corepack enable
corepack prepare pnpm@9.15.0 --activate
这样你就能避免“你用 pnpm 8 我用 pnpm 9”的隐形分叉。
5. monorepo 体验:为什么我更偏向 pnpm?
你这个仓库已经有 pnpm-workspace.yaml,说明它是一个典型的 workspace/monorepo 结构。
在 monorepo 下,你最想要的通常是:
- 依赖安装快
- 重复依赖少
- workspace 之间的依赖边界清晰
- lockfile 稳定
pnpm 在这些方面天然优势很明显。
5.1 workspace 依赖更“像你写的那样”
pnpm 对 workspace 依赖的处理更严格:
- 你没声明的依赖就不该 import 到
这会逼着你把依赖写对:
- 好处:长期稳定,避免幽灵依赖
- 代价:短期会暴露出一些历史“脏 import”
但我更愿意把它当成一次免费的代码体检。
5.2 pnpm 的坑:不是没有,只是换了地方
pnpm 最常见的“第一天坑”:
- 某些老工具默认假设 node_modules 是扁平的
- 结果它在运行时找不到依赖
解决思路通常是:
- 修正依赖声明(该加 dependencies 就加)
- 或者使用 pnpm 的 hoist 配置(权衡严格性)
如果你一上来就把 pnpm 彻底 hoist 成 npm 的样子,那你等于白用 pnpm。
6. 真实项目里怎么落地:一份“别踩坑清单”
6.1 团队统一:只允许一个包管理器
最常见的灾难是:
- A 用 npm 生成
package-lock.json - B 用 pnpm 生成
pnpm-lock.yaml - C 用 yarn 生成
yarn.lock
最后仓库里出现三个锁文件,CI 直接人格分裂。
建议:
- 只保留一个 lockfile
- 写进 README/CONTRIBUTING
- CI 校验多余锁文件直接失败
6.2 安装命令标准化
- 本地:
pnpm i - CI:
pnpm install --frozen-lockfile
6.3 依赖声明要干净
- 用到的就写进
dependencies/devDependencies - 不要依赖“hoist 帮你把它放到顶层”
这条看起来像废话,但它能消灭一大票“在我电脑上能跑”的问题。
7. 面试怎么讲:别背“谁更快”,要讲“为什么”
你可以这样组织答案:
- 三者目标相同:解析依赖图 + 落盘 + 锁定版本。
- 差异核心:node_modules 的组织方式与依赖隔离策略。
- npm/yarn(v1) 更倾向 hoist,兼容性强但可能幽灵依赖。
- pnpm 用全局 store + 链接复用,更省空间更严格,monorepo 体验好。
- yarn berry 的 PnP 可以不要 node_modules,极致严格与快速,但需要工具链配合。
这套回答比“我觉得 pnpm 更好”更像工程师。
总结
npm:最稳、最通用,适合小团队/简单项目。yarn:Classic 兼容路线,Berry(PnP) 极致路线,看团队与工具链。pnpm:复用与严格兼得,特别适合 monorepo 与依赖多的项目。
小明金句收尾:
选包管理器不是选“最强的”,是选“最适合你团队约束的”。