npm、pnpm和Bun怎么选?依赖隔离、安装速度和迁移成本对比
选包管理工具不是比谁安装快——安装只在 npm install 那一瞬间,但依赖结构、磁盘占用、monorepo 支持、CI 表现才是日常影响效率的因素。这篇文章不列参数表,而是从实际场景出发:你的项目该用哪个,什么时候该迁移。
三者的核心差异
| npm | pnpm | Bun | |
|---|---|---|---|
| 依赖存储 | 每个项目独立安装 | 全局存储 + 硬链接 | 每个项目独立安装 |
| node_modules 结构 | 扁平化(包可访问未声明的依赖) | 嵌套 + 符号链接(严格隔离) | 扁平化 |
| monorepo 支持 | npm workspaces(npm 7+) | pnpm workspaces + 过滤器 | Bun workspaces |
| 锁文件 | package-lock.json | pnpm-lock.yaml | bun.lockb(二进制) |
| Node.js 依赖 | 自带(npm 就是 Node 的一部分) | 需要 Node.js | 自带运行时(可替代 Node) |
pnpm:省磁盘、严格隔离
为什么省磁盘
npm 和 Yarn 每个项目都把依赖完整安装到 node_modules,10 个项目用同一个版本的 lodash,磁盘上就有 10 份。pnpm 把所有包存到全局存储 ~/.pnpm-store,项目里的 node_modules 通过硬链接指向全局存储——10 个项目只有 1 份 lodash 的实际文件。
bash# 查看全局存储位置 pnpm store path # 查看存储占用的磁盘空间 pnpm store prune # 清理未被引用的包
实际节省:一个 10 个前端项目的机器,npm 可能占 5GB 的 node_modules,pnpm 只要 1-2GB。
依赖隔离才是重点
npm 的扁平化 node_modules 允许你引用未在 package.json 里声明的依赖——因为 npm 会把所有包提升到顶层。这叫"幻影依赖",代码能跑但不知道为什么能跑。
javascript// 你的 package.json 只声明了 express // 但代码里直接用了 express 的依赖 debug const debug = require('debug'); // npm 下能跑,pnpm 下报错
pnpm 的 node_modules 结构是这样的:
shellnode_modules/ ├── .pnpm/ │ ├── express@4.18.2/ │ │ └── node_modules/ │ │ ├── express/ # 硬链接 │ │ └── debug/ # 只有 express 能访问 │ └── debug@4.3.4/ │ └── node_modules/ │ └── debug/ └── express/ # 符号链接到 .pnpm/express
debug 在 express 的 node_modules 里,不在项目顶层。你的代码直接 require('debug') 会报 MODULE_NOT_FOUND——必须自己声明依赖。
这个"严格模式"是 pnpm 最大的价值:提前发现依赖声明缺失,而不是上线后因为某个间接依赖升级而突然崩溃。
pnpm 的 monorepo 过滤器
pnpm 的 --filter 是 monorepo 管理最强的功能:
bash# 只构建依赖了 shared 包的包 pnpm --filter ...shared build # 只构建 app 包及其所有依赖 pnpm --filter app... build # 排除某个包 pnpm -r build --filter=!docs
... 语法表示"依赖链"——比 npm 的 -w 灵活得多。
什么时候用 pnpm
- monorepo 项目(过滤器和严格隔离是刚需)
- 磁盘空间有限(CI 服务器、Docker 镜像)
- 想要严格的依赖边界(大型团队、长期维护项目)
- 从 npm 迁移成本:需要加
pnpm-workspace.yaml,.npmrc里的shamefully-hoist=true可以兼容旧代码
Bun:最快,但有取舍
Bun 不只是包管理器——它是 Node.js 的替代运行时,内置包管理器、测试框架、打包工具。
安装速度
Bun 的安装速度确实碾压其他工具——用 Zig 写的,多线程解析 package.json,全局缓存 + 硬链接。实际测试(cold install,~500 依赖):
- npm:~45s
- pnpm:~25s
- Bun:~8s
CI 环境下差距缩小(有缓存时 npm 和 pnpm 也不慢),本地开发反复 rm -rf node_modules && install 时差距最明显。
运行时差异
Bun 的运行时兼容大部分 Node.js API,但不完全兼容:
- 兼容的:
fs、path、http、crypto、大部分 npm 包 - 不兼容的:Node.js 的 C++ 原生模块(如
node-gyp编译的包)需要特殊处理;部分child_process行为差异;worker_threads支持不完整 - 原生支持的:TypeScript 直接运行(不需
ts-node)、JSX、.env文件、WebSocket
什么时候用 Bun
- 新项目,不需要 C++ 原生模块
- 对启动速度和安装速度有极致要求
- 愿意用 Bun 做运行时而不仅仅是包管理器
- 不适合:依赖
node-gyp编译的包(如better-sqlite3、canvas)、需要完整 Node.js 兼容性的项目
Bun 的锁文件是二进制的
bun.lockb 是二进制格式,git diff 看不到变化内容,code review 不友好。这是迁移到 Bun 的常见顾虑。可以用 bun lockfile 导出为可读格式。
npm:兼容性最好的默认选择
npm 的最大优势:不需要额外安装。Node.js 自带 npm,所有 Node 项目开箱即用。
npm 的局限
- 安装速度最慢(单线程解析,无全局缓存复用)
- 扁平化 node_modules 导致幻影依赖
- monorepo 的
-w过滤能力比 pnpm--filter弱很多 package-lock.json合并冲突频发
什么时候坚持用 npm
- 小型项目,依赖少于 50 个
- 团队不熟悉 pnpm/Bun,不想引入新工具
- 需要最大兼容性(某些 CI 环境只预装 npm)
- 临时项目、原型验证
迁移建议
npm → pnpm
bash# 安装 pnpm npm install -g pnpm # 在项目根目录执行 pnpm import # 自动从 package-lock.json 生成 pnpm-lock.yaml # 安装依赖 pnpm install # 如果有幻影依赖报错,临时加 shamefully-hoist echo "shamefully-hoist=true" > .npmrc pnpm install
shamefully-hoist=true 让 pnpm 的 node_modules 结构和 npm 一样扁平,兼容旧代码。后续逐步补齐缺失的依赖声明后去掉这个配置。
npm → Bun
bash# 安装 Bun curl -fsSL https://bun.sh/install | bash # 在项目根目录 bun install # 自动从 package-lock.json 生成 bun.lockb
注意检查 C++ 原生模块的兼容性。bcrypt、sharp、canvas 等包可能需要额外配置。
选择决策树
shell项目需要 C++ 原生模块? ├─ 是 → npm 或 pnpm └─ 否 → 是 monorepo 吗? ├─ 是 → pnpm(过滤器 + 严格隔离) └─ 否 → 追求开发体验和速度吗? ├─ 是 → Bun(安装快、启动快、TS 原生支持) └─ 否 → npm(零配置,开箱即用)