返回博客列表
技术文章Node.js包管理器npmYarnpnpmBun工程化

npm、Yarn、pnpm 和 Bun 到底有什么区别

2026年05月🇨🇳 中文

前端项目里,包管理器很容易被当成一个“安装依赖的工具”。

于是讨论 npm、Yarn、pnpm、Bun 的时候,最后经常只剩下一句话:谁更快。

但包管理器真正影响项目的地方,远不止安装速度。它会影响依赖如何被解析、node_modules 如何组织、lockfile 是否稳定、幽灵依赖会不会被放过、monorepo 工作区怎么管理、CI 缓存怎么设计,甚至会影响团队成员本地环境能不能保持一致。

所以这篇文章不打算只做跑分比较,而是从工程角度看这四个工具到底差在哪。

npm:默认选项,也是生态基线

npm 是 Node.js 默认自带的包管理器。

它最大的优势不是某个单点能力,而是“默认”。几乎所有 Node.js 开发者都知道它,几乎所有包都以 npm registry 为分发中心,几乎所有工具链都默认兼容 npm 的行为。

这件事在工程里很重要。

当一个项目使用 npm 时,团队协作成本最低。新人不用安装额外工具,CI 环境也通常默认支持。对于不复杂的单仓库项目,npm 已经足够稳定。

但 npm 早期也留下过一些历史包袱,其中最典型的是 node_modules 的扁平化结构。

为了避免过深的依赖树,npm 会尽量把依赖提升到项目根部的 node_modules 中。这样做能减少路径层级,也能提升兼容性,但副作用是:项目代码有时候可以引用到自己没有显式声明的依赖。

这就是常说的幽灵依赖。

比如你的项目没有在 package.json 里声明 lodash,但某个依赖 A 间接依赖了 lodash,而 npm 又把 lodash 提升到了根目录。此时你的代码里 import lodash from 'lodash' 可能是能跑的。

问题是,这种可运行是偶然的。一旦依赖 A 升级或移除,lodash 消失,你的项目才会暴露问题。

npm 现在已经比早期成熟很多,lockfile、workspace、缓存和安装性能都有明显改善。但它的定位仍然更像生态默认基线:稳定、通用、兼容性好,但在依赖严格性和磁盘效率上不是最激进的。

Yarn:最初是为了解决 npm 的确定性和速度问题

Yarn 最早流行起来,是因为早期 npm 在安装速度和确定性上确实有不少问题。

当年 npm 的 lockfile 还不够成熟,不同机器、不同时间安装出来的依赖结果可能不一致。Yarn 通过 yarn.lock、并行下载和更稳定的缓存机制,解决了很多团队协作中的痛点。

所以 Yarn 一开始的价值很清晰:让依赖安装更快、更确定。

后来 npm 自己补上了 lockfile 和很多性能改进,Yarn 的差异化就转向了另一个方向:Yarn Berry,也就是 Yarn 2+。

Yarn Berry 最大的变化是 Plug'n'Play,简称 PnP。

PnP 的思路很激进:不要再生成传统 node_modules。依赖包存放在缓存中,运行时通过一个映射文件告诉 Node.js 该从哪里加载包。

这样做有几个明显好处:

  • 安装速度快
  • 磁盘占用低
  • 依赖关系更严格
  • 不容易出现幽灵依赖

但它的代价也很明显:生态兼容性。

因为 JavaScript 生态里大量工具都默认假设存在 node_modules。一些老工具、脚本、构建插件、编辑器集成,可能会因为 PnP 的解析方式而出问题。虽然 Yarn 提供了很多兼容方案,但团队需要理解这套模型,否则排查问题时会比较痛苦。

所以今天看 Yarn,我会把它分成两种语境:

  • Yarn Classic,也就是 Yarn 1,更像 npm 的增强版。
  • Yarn Berry,也就是 Yarn 2+,更像一套更严格、更现代但也更有学习成本的依赖管理方案。

如果一个团队已经深度使用 Yarn workspace 或 PnP,并且工具链兼容良好,那它完全可以继续稳定使用。但如果是新项目,我通常不会因为“Yarn 更快”就默认选择 Yarn,因为这个优势已经不像早期那么明显。

pnpm:用内容寻址存储解决重复安装问题

pnpm 是我现在更愿意在中大型前端项目里优先考虑的包管理器。

它最核心的特点不是“快”,而是依赖存储模型不同。

npm 和 Yarn Classic 会把依赖复制到项目的 node_modules 里。不同项目如果都依赖 React,那每个项目都会有一份自己的 React。

pnpm 的做法是:把包内容存到全局内容寻址仓库中,然后在项目的 node_modules 里通过硬链接和符号链接指向这些真实文件。

简单理解:

全局 store:
  react@19.2.1
  typescript@5.9.3

项目 A node_modules:
  react -> 链接到全局 store
  typescript -> 链接到全局 store

项目 B node_modules:
  react -> 链接到同一份全局 store

这样做带来两个直接收益:

  1. 磁盘占用更低。
  2. 安装速度更快,因为重复包不需要反复下载和复制。

但 pnpm 更重要的优点是:它的 node_modules 结构更严格。

pnpm 不会像 npm 那样随意把间接依赖提升到根目录。默认情况下,项目只能访问自己在 package.json 里声明过的依赖。

这会让某些历史项目第一次迁移到 pnpm 时直接报错。

表面看是 pnpm 更麻烦,实际上是 pnpm 把项目原本就存在的问题暴露出来了:你代码里使用了没有声明的依赖。

这也是我喜欢 pnpm 的原因之一。它会逼你把依赖关系写清楚。

在 monorepo 场景里,pnpm 的 workspace 也很成熟。它可以很好地处理多个 package 之间的本地依赖、统一 lockfile、依赖复用和安装缓存。对于组件库、前后端混合仓库、工具包集合这类项目,pnpm 的体验通常很好。

当然,pnpm 也不是没有成本。

因为它的依赖结构更严格,某些写得不规范的第三方包可能会暴露问题。比如某个包内部使用了自己没有声明的 peer dependency 或间接依赖,在 npm 下可能侥幸能跑,在 pnpm 下就会失败。

所以 pnpm 更适合愿意维护依赖边界的团队。

如果团队希望“历史项目先别动,能跑就行”,pnpm 迁移可能会踩一些坑;如果团队本来就重视工程健康,pnpm 反而能帮你更早发现问题。

Bun:它不只是包管理器,而是一整套 JavaScript 工具链

Bun 和前面三个工具不太一样。

npm、Yarn、pnpm 本质上都是 Node.js 生态里的包管理器。Bun 虽然也提供 bun install,但它的野心更大:它同时是 JavaScript runtime、包管理器、脚本运行器、测试工具和打包工具。

这也是讨论 Bun 时最容易混淆的地方。

如果只看安装依赖,Bun 很快。它用 Zig 写,安装器性能很好,lockfile 是 bun.lockb 或新版文本 lockfile,执行 bun install 通常非常快。

但 Bun 的差异不只在安装阶段。

当你用:

bun run dev

你可能已经不只是换了包管理器,而是在用 Bun 的运行时执行脚本。大多数情况下这没问题,但如果项目或依赖依赖了某些 Node.js 边界行为,Bun 的兼容性就需要确认。

Bun 对很多 Node.js API 已经兼容得不错,但“兼容不错”和“完全等价”不是一回事。尤其是一些依赖底层 Node 行为、原生模块、构建脚本、复杂 CLI 的项目,仍然需要实际验证。

所以我对 Bun 的看法比较谨慎:

  • 新项目、个人项目、工具项目,可以大胆尝试。
  • 对性能敏感的本地开发流程,可以考虑使用。
  • 生产级大型项目要先确认依赖兼容性。
  • 如果只是想换包管理器,不一定要顺手把 runtime 也换掉。

Bun 的优势是未来感很强,体验也很快;它的风险是边界更宽,因为你引入的可能不是一个包管理器,而是一整套替代 Node 工具链的方案。

lockfile:团队协作里真正不能忽略的东西

包管理器的一个核心职责,是让不同机器安装出同一棵依赖树。

这就是 lockfile 的意义。

不同工具的 lockfile 不一样:

工具lockfile
npmpackage-lock.json
Yarnyarn.lock
pnpmpnpm-lock.yaml
Bunbun.lock / bun.lockb

lockfile 应该提交到仓库。

如果一个项目里同时出现多个 lockfile,就要小心了。比如既有 package-lock.json,又有 pnpm-lock.yaml,还混了 bun.lock。这通常意味着团队里有人用不同工具装过依赖,依赖树可能已经不一致。

我的建议很简单:

一个项目只保留一个包管理器,一个 lockfile。

这件事比选择哪个工具更重要。

如果项目决定使用 pnpm,就用 pnpm install;如果决定使用 Bun,就用 bun install。不要今天 npm、明天 yarn、后天 bun。否则你调试的可能不是代码问题,而是依赖树漂移问题。

性能对比不能只看“安装快不快”

包管理器性能通常被简化成安装速度,但工程里至少要分几种情况看。

第一种是冷安装:没有缓存、没有 node_modules,从头下载依赖。

第二种是热安装:缓存已经存在,只需要校验和链接。

第三种是 CI 安装:依赖缓存、lockfile 校验、网络稳定性都会影响结果。

第四种是 monorepo 安装:多个 package 共享依赖,依赖复用能力很重要。

在这些场景里,通常可以这样粗略理解:

  • npm:稳定通用,性能已经够用,但不是最省磁盘。
  • Yarn Classic:历史上比 npm 快,现在优势不明显。
  • Yarn Berry:PnP 模式下很快,但需要生态兼容。
  • pnpm:安装快、磁盘省、monorepo 表现好。
  • Bun:非常快,但要同时关注运行时兼容。

如果只是一个普通小项目,性能差异未必值得你为了它迁移工具链。

但如果你维护的是大型 monorepo,或者经常在 CI 里安装大量依赖,pnpm 或 Bun 带来的时间差就会变得很明显。

monorepo 场景下,我更偏向 pnpm

如果是单个简单应用,npm、pnpm、Bun 都能胜任,差别没有很多文章说得那么夸张。

但如果是 monorepo,我会更偏向 pnpm。

原因有几个。

首先,pnpm workspace 的模型很清楚,多个 package 之间可以通过 workspace 协议建立本地依赖:

{
  "dependencies": {
    "@repo/ui": "workspace:*"
  }
}

其次,pnpm 的全局 store 对 monorepo 很友好。同一仓库里多个包依赖同一个版本的 React、TypeScript、ESLint,不需要重复复制。

再者,它的依赖严格性会让 monorepo 的边界更清楚。每个 package 用了什么,就应该自己声明什么,而不是靠根目录偶然存在的依赖活着。

这对长期维护很重要。

monorepo 最怕的不是一开始跑不起来,而是半年以后某个子包到底依赖谁已经没人说得清。

什么时候选哪个

如果让我给一个比较实际的建议,我会这么选:

普通 Node.js / Next.js 项目

用 npm 或 pnpm 都可以。

如果团队没有特别偏好,npm 胜在默认和稳定;如果你希望依赖更严格、安装更省磁盘,我会选 pnpm。

中大型前端项目或 monorepo

优先考虑 pnpm。

它的 workspace、安装性能、磁盘复用和依赖严格性都比较适合长期维护。

已经深度使用 Yarn 的老项目

不必为了追新迁移。

如果 Yarn 1 跑得稳定,可以继续用;如果是 Yarn Berry + PnP,要确保团队理解它的解析模型,并且工具链兼容。

个人项目、脚手架、工具项目

可以尝试 Bun。

Bun 的速度和一体化体验确实很好。如果项目边界可控,依赖不复杂,用起来会很舒服。

对生产稳定性要求很高的项目

不要只因为 Bun 快就立刻切。

先确认依赖、构建脚本、测试、部署和运行时行为都没有兼容问题。如果只是想提升安装速度,pnpm 可能是更稳的选择。

总结

npm、Yarn、pnpm 和 Bun 的区别,不只是“谁安装更快”。

npm 是生态默认值,胜在通用和稳定。
Yarn 解决过 npm 早期的确定性和速度问题,Yarn Berry 又进一步尝试用 PnP 改变依赖解析模型。
pnpm 用内容寻址 store 和更严格的 node_modules 结构,在性能、磁盘占用和依赖边界之间取得了很好的平衡。
Bun 则不只是包管理器,它试图提供一整套更快的 JavaScript 工具链。

如果是我做选择:

  • 简单项目:npm 或 pnpm。
  • 中大型项目:pnpm。
  • monorepo:pnpm。
  • 想尝鲜或做工具:Bun。
  • 已有 Yarn 项目:稳定优先,不必为了换而换。

包管理器不是越新越好,也不是越快越好。

真正重要的是:它能不能让你的依赖关系更清楚,让团队环境更一致,让项目在半年后还能被人稳稳地装起来。