npm、Yarn 和 pnpm 有什么区别?pnpm 为什么安装更快、磁盘占用更少?
简化版
npm、Yarn、pnpm 都是 JavaScript 包管理器,差异主要在依赖解析、lockfile、安装策略、缓存和 workspace 支持。pnpm 通过内容寻址存储和硬链接/符号链接组织依赖,多个项目可复用同一份包内容,因此磁盘占用更少,也能减少重复下载和复制。
详细版
npm 是默认生态入口,兼容性最好;Yarn 曾重点解决早期 npm 的速度和确定性问题,现代版本引入 PnP 等能力;pnpm 强调严格依赖访问、全局内容寻址 store、workspace 和高效安装。
pnpm 的 node_modules 看起来存在依赖目录,但真实包内容通常存储在全局 store 中,项目里通过链接引用。这样 10 个项目依赖同一版本 React 时,不需要复制 10 份完整文件。
面试回答要补充 trade-off:pnpm 更严格,能暴露幽灵依赖问题;但某些老工具如果假设扁平 node_modules,可能需要调整配置。
完整版教学
一、包管理器解决的不只是下载依赖
包管理器至少负责 5 件事:解析版本范围、下载包、生成确定性 lockfile、组织 node_modules、执行生命周期脚本和 workspace 任务。不同工具差异主要就在这些环节。
package.json -> resolve versions -> lockfile
-> fetch packages -> link node_modules
-> run scripts
因此比较 npm、Yarn、pnpm 不能只说“谁更快”。要看项目规模、团队生态、CI 缓存、monorepo 需求、工具兼容和安全策略。
二、npm 的优势是默认和兼容
npm 随 Node.js 分发,是 JavaScript 生态默认入口。对于普通应用,npm 的学习成本和工具兼容性最好,很多文档也默认给 npm 命令。
现代 npm 已经有 lockfile、workspaces、overrides 等能力,不再是早期那个安装慢且不确定的形象。面试里如果把 npm 简单说成“落后”,会显得答案过时。
| 维度 | npm 特点 |
|---|---|
| 默认性 | Node 自带 |
| 兼容性 | 生态文档最普遍 |
| lockfile | package-lock.json |
| workspace | 支持 |
| 适用 | 中小项目、兼容优先团队 |
三、Yarn 的不同版本要分开说
Yarn Classic 主要改善早期 npm 的速度、缓存和确定性;Yarn Berry 引入 Plug’n’Play、零安装等更激进能力。很多团队说“用 Yarn”,但实际可能指完全不同的版本。
PnP 不再依赖传统 node_modules 目录查找,而是通过映射文件解析依赖。这能提高确定性并减少文件数量,但也要求工具链兼容 PnP。
面试时可以这样说:Yarn 的优势不只是速度,关键是它对依赖确定性和安装模型做过大量探索;但版本差异大,落地前要确认团队使用的是 Classic 还是 Berry。
四、pnpm 为什么磁盘占用少
pnpm 使用内容寻址 store。相同包内容只存一份,不同项目通过链接指向它。假设 8 个项目都依赖 react@18.2.0,传统复制模型可能有 8 份,pnpm store 里只保留一份内容。
记忆钩子:pnpm 不是每个项目复制一套包,而是让多个项目链接到同一份内容存储。
global pnpm store
react@18.2.0 <--- project-a/node_modules/.pnpm/...
<--- project-b/node_modules/.pnpm/...
<--- project-c/node_modules/.pnpm/...
如果一个依赖包解压后 5 MB,10 个项目都用同一版本,重复复制约 50 MB;内容寻址复用后主体内容只存 5 MB 左右,项目里主要是链接和少量元数据。
五、pnpm 的严格依赖能暴露幽灵依赖
幽灵依赖指项目没有在 package.json 声明,却因为别的包间接安装了它而可以 import。扁平化 node_modules 容易让这种问题潜伏。
// package.json 没声明 lodash,但代码直接使用
import debounce from 'lodash/debounce';
某天上游依赖移除了 lodash,项目就会突然坏。pnpm 的依赖结构更严格,默认只允许访问自己声明的依赖,因此能更早暴露这种不规范引用。
六、lockfile 和 CI 行为要重点关注
团队协作时,lockfile 保证同一份依赖树可复现。CI 中通常要求 lockfile 与 package.json 一致,否则应该失败,而不是悄悄更新依赖。
| 工具 | 常见锁文件 | CI 常见命令 |
|---|---|---|
| npm | package-lock.json | npm ci |
| Yarn | yarn.lock | yarn install —immutable |
| pnpm | pnpm-lock.yaml | pnpm install —frozen-lockfile |
如果一个 PR 改了 package.json 却没提交 lockfile,CI 应该拦住。否则本地、测试和生产构建可能拿到不同依赖版本。
七、选型不能只看安装速度
pnpm 很适合 monorepo 和依赖体量大的项目;npm 适合兼容和默认成本优先;Yarn 适合已有生态和需要特定安装模型的团队。真正选型要做 PoC。
PoC 至少比较 4 个指标:冷安装时间、热安装时间、磁盘占用、工具兼容问题。比如 pnpm 冷安装从 240 秒降到 110 秒很诱人,但如果核心脚手架依赖扁平结构导致大量适配,也要把迁移成本算进去。
包管理器还会影响安全审计、私有源、workspace 发布、patch 依赖和缓存策略。选型是工程治理问题,不是命令风格偏好。
八、常见误区与追问
- 误区:pnpm 快只是因为网络下载快。 它的内容寻址 store、链接机制和缓存复用都影响速度与磁盘占用。
- 误区:npm 一定不适合大型项目。 现代 npm 已支持 lockfile、workspace 和 overrides,很多项目完全够用。
- 误区:幽灵依赖没问题,只要本地能跑。 它会让依赖关系不可复现,上游变化后容易突然出故障。
- 追问:pnpm 为什么更严格? 它让项目默认只能访问声明过的依赖,减少扁平 node_modules 带来的隐式依赖。
- 追问:CI 为什么要 frozen lockfile? 为了确保安装结果和提交的 lockfile 一致,避免构建时偷偷改依赖树。
- 追问:monorepo 里为什么常用 pnpm? workspace、内容寻址 store 和递归任务能力对多包项目更友好。
九、加强记忆
比较包管理器按“解析、存储、链接、锁定、治理”回答:npm 胜在默认兼容,Yarn 要区分 Classic 和 Berry,pnpm 通过内容寻址 store 和链接机制节省磁盘并提高复用,还能暴露幽灵依赖。最后用 CI lockfile 和工具兼容作为工程落地收尾。