Chapter 01

为什么不用 npm / Yarn

npm 是 Node 官方包管理,Yarn 是 Facebook 2016 做的替代——两家都采用「扁平 node_modules」,解决了依赖地狱,也埋下了新雷区。pnpm 2017 年诞生就是冲着这些雷去的。

npm v1 时代的嵌套地狱

node_modules/
├── react/
│   └── node_modules/
│       └── object-assign/
├── react-dom/
│   └── node_modules/
│       └── object-assign/       ← 重复一份
└── lodash/

2015 年之前,npm 严格嵌套:每个包有自己的 node_modules。优点是每个依赖看到的版本绝对正确,缺点是重复太多——Windows 还因路径过长崩溃。

npm v3 / Yarn 的扁平化

node_modules/
├── react/
├── react-dom/
├── object-assign/              ← 提升到顶层
├── lodash/
└── ...(所有依赖混在一起)

2016 年起,npm 和 Yarn 默认扁平化:能提就提。磁盘好多了,但带来了新问题。

问题一:幽灵依赖(phantom dependencies)

// 你的 package.json
{
  "dependencies": {
    "react": "^18.0.0"
  }
}
// 你的代码
import _ from 'lodash';  // ← 你没装 lodash!
_.debounce(...);        // 居然能运行

为什么?react 某个子依赖装了 lodash,被扁平化提到顶层——Node 的模块解析能找到,你以为自己装了。直到某天 react 升版不依赖 lodash 了,线上炸掉

这不是假设
Vercel 2021 年一次 Next.js 升级,打包后产物依赖了 @babel/types(实际是其它包的子依赖,从没显式声明),升级后找不到,全网 build 红。这种事故在大型单仓里几乎是月更频率。

问题二:磁盘重复

~/projects/project-a/node_modules    ← 600MB
~/projects/project-b/node_modules    ← 600MB (80% 和 A 重复)
~/projects/project-c/node_modules    ← 600MB

笔记本 SSD 动辄 256GB,装十个项目 6GB 没了。开 du -sh 去找罪魁祸首,永远是 node_modules。

问题三:非确定性提升

同样 package.json,npm 和 Yarn 提升的结果可能不同,甚至同一工具不同版本结果都可能变——这意味着「本地能跑、CI 跑不通」时,复现很难。

pnpm 的三招

1. 内容寻址仓库(CAS)
所有包只存一份在 ~/.local/share/pnpm/store,按文件内容哈希索引。相同文件跨项目跨版本共享,存 100 次 React 只占 1 份空间。
2. 硬链接(hard link)到 node_modules
从全局 store 链接到项目,不复制文件——瞬间完成,不占额外空间。
3. 严格 symlink 布局
项目 node_modules/ 顶层只有你 package.json 声明的包。子依赖藏在 node_modules/.pnpm/——幽灵依赖直接报错。

对比:装个 Next.js 项目

npmYarn v1pnpm
首次 install75s55s30s
二次 install(改包)35s25s8s
磁盘占用(10 个项目)6.0 GB5.8 GB1.2 GB
幽灵依赖✅ 有✅ 有❌ 没有
Monorepo workspacesv7 原生原生原生且最快
Catalog 版本对齐✅(v9)

Yarn Berry(v2+)怎么样

Yarn 2020 年推出 Berry,用 Plug'n'Play(PnP)完全绕过 node_modules——更激进,确实快。但:

pnpm 走中间路线:保留 node_modules 布局、生态全兼容,同时用 store+symlink 拿到 PnP 的好处。实际项目里 pnpm 胜出。

生态现状(2026)

什么时候用 pnpm

推荐场景

什么时候暂缓

Bun 和 Deno 呢

三年内 JS 社区会有多种包管理工具并存,pnpm 是目前兼容性和功能性最好的平衡点。

本章小结