6月4日 23:08

npm、pnpm和Bun怎么选?依赖隔离、安装速度和迁移成本对比

选包管理工具不是比谁安装快——安装只在 npm install 那一瞬间,但依赖结构、磁盘占用、monorepo 支持、CI 表现才是日常影响效率的因素。这篇文章不列参数表,而是从实际场景出发:你的项目该用哪个,什么时候该迁移。

三者的核心差异

npmpnpmBun
依赖存储每个项目独立安装全局存储 + 硬链接每个项目独立安装
node_modules 结构扁平化(包可访问未声明的依赖)嵌套 + 符号链接(严格隔离)扁平化
monorepo 支持npm workspaces(npm 7+)pnpm workspaces + 过滤器Bun workspaces
锁文件package-lock.jsonpnpm-lock.yamlbun.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 结构是这样的:

shell
node_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,但不完全兼容:

  • 兼容的fspathhttpcrypto、大部分 npm 包
  • 不兼容的:Node.js 的 C++ 原生模块(如 node-gyp 编译的包)需要特殊处理;部分 child_process 行为差异;worker_threads 支持不完整
  • 原生支持的:TypeScript 直接运行(不需 ts-node)、JSX、.env 文件、WebSocket

什么时候用 Bun

  • 新项目,不需要 C++ 原生模块
  • 对启动速度和安装速度有极致要求
  • 愿意用 Bun 做运行时而不仅仅是包管理器
  • 不适合:依赖 node-gyp 编译的包(如 better-sqlite3canvas)、需要完整 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++ 原生模块的兼容性。bcryptsharpcanvas 等包可能需要额外配置。

选择决策树

shell
项目需要 C++ 原生模块? ├─ 是 → npmpnpm └─ 否 → 是 monorepo 吗? ├─ 是 → pnpm(过滤器 + 严格隔离) └─ 否 → 追求开发体验和速度吗? ├─ 是 → Bun(安装快、启动快、TS 原生支持) └─ 否 → npm(零配置,开箱即用)
标签:NPM