npm vs yarn vs pnpm:包管理器怎么选

三个包管理器到底差在哪?从 node_modules 结构、锁文件、依赖解析、monorepo 支持、性能与坑点出发,小明给你一份可落地的选择指南:什么时候用 npm,什么时候用 yarn,什么时候该上 pnpm。

18 分钟阅读
小明

npm vs yarn vs pnpm:包管理器怎么选

你团队里一定出现过这种对话:

  • A:“我们用 npm 就行,官方的最稳。”
  • B:“yarn 更快啊,装依赖像开了挂。”
  • C:“pnpm 才是未来,磁盘占用直接砍半。”

然后争论会迅速退化成宗教战争:

  • “我一直用它,从没出过问题。”
  • “别人都在用,肯定更好。”
  • “你不用就是落后。”

但作为工程师,我们不靠信仰选工具,我们靠:约束、机制、权衡

今天这篇文章我不想给你一个“标准答案”,而是给你一套选择框架:

  • 你能解释它们差在哪
  • 你能知道你的项目更需要什么
  • 你能在 CI/团队协作/monorepo 下少踩坑

1. 先把结论放前面:怎么选最快?

如果你只想要一个“先用起来”的答案:

  • 单仓库、小团队、追求稳定:选 npm
  • 需要 Yarn Berry(PnP)、强约束与现代工作流:选 yarn
  • monorepo / 依赖多 / 想要更快更省磁盘:选 pnpm

但别急着关页面。

因为你真正要做的是:在下面 5 个维度里,找到你项目的优先级:

  1. 安装性能(快不快)
  2. 磁盘占用(省不省)
  3. 依赖隔离(会不会“偷吃”依赖)
  4. 锁文件与可复现构建(CI 能不能稳定复现)
  5. monorepo 体验(workspace/hoist/版本策略)

2. 三者共同点:它们都在做同一件事

无论 npm/yarn/pnpm,核心流程都类似:

  1. package.json
  2. 解析依赖图(版本范围、peerDependencies、optionalDependencies…)
  3. 选择具体版本(resolution)
  4. 下载包(cache)
  5. 把包放进项目可用的位置(node_modules 或 PnP)
  6. 写锁文件,保证可复现

所以差异主要出现在两个地方:

  • “依赖怎么落盘”(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 不是直接复制包
  • 而是通过硬链接/符号链接把包“挂”进来

这带来三个非常实在的结果:

  1. 安装快(大量复用)
  2. 磁盘省(不重复存)
  3. 依赖更严格(默认不随便 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

这能保证:

  • 锁文件不一致就直接失败
  • 防止有人本地安装把版本悄悄漂移

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. 面试怎么讲:别背“谁更快”,要讲“为什么”

你可以这样组织答案:

  1. 三者目标相同:解析依赖图 + 落盘 + 锁定版本。
  2. 差异核心:node_modules 的组织方式与依赖隔离策略。
  3. npm/yarn(v1) 更倾向 hoist,兼容性强但可能幽灵依赖。
  4. pnpm 用全局 store + 链接复用,更省空间更严格,monorepo 体验好。
  5. yarn berry 的 PnP 可以不要 node_modules,极致严格与快速,但需要工具链配合。

这套回答比“我觉得 pnpm 更好”更像工程师。


总结

  • npm:最稳、最通用,适合小团队/简单项目。
  • yarn:Classic 兼容路线,Berry(PnP) 极致路线,看团队与工具链。
  • pnpm:复用与严格兼得,特别适合 monorepo 与依赖多的项目。

小明金句收尾:

选包管理器不是选“最强的”,是选“最适合你团队约束的”。