一句话理解

pnpm(Performant npm)是 Node.js 生态中的包管理器。它与 npm 使用相似的命令,但重新设计了依赖包的存储、链接和项目内布局,因此在磁盘占用、安装速度、依赖隔离和 monorepo 管理方面有明显特点。

pnpm 的核心价值不是“命令更短”,而是让多个项目共享依赖文件,同时让项目只能使用自己明确声明的依赖。

1. 内容寻址存储:更省磁盘

npm 常见的做法是把依赖包复制到每个项目的 node_modules 中。如果电脑上有很多项目,它们可能会重复保存大量相同文件。

pnpm 使用内容寻址存储(Content-Addressable Storage, CAS):

全局 store
├── react 的文件
├── lodash 的文件
└── 其他依赖的文件

项目 A/node_modules ──链接──> 全局 store
项目 B/node_modules ──链接──> 全局 store

安装包时,pnpm 将文件保存到全局 store,项目中的 node_modules 通过 hard link 或 reflink 使用这些文件,而不是再次复制完整内容。

这带来几个结果:

  • 多个项目使用同一版本依赖时可以复用文件;
  • 项目越多,重复占用的磁盘空间越少;
  • 同一包的不同版本之间也可以复用没有变化的文件;
  • 已经下载过的依赖再次安装时,通常可以跳过下载和完整复制。

可以用下面的命令查看 store 位置:

pnpm store path

2. 默认依赖隔离:减少幽灵依赖

传统扁平化 node_modules 容易出现幽灵依赖(Phantom Dependency):项目没有在 package.json 中声明某个包,但因为另一个传递依赖把它提升到了可访问的位置,项目却意外地可以 import 它。

这种代码可能暂时能运行:

// package.json 没有声明 lodash
import _ from 'lodash'

但当上游依赖升级、依赖树重新排列时,代码就可能突然失效。

pnpm 默认采用更严格的依赖布局。一个包通常只能访问自己 package.json 中声明的依赖,因此依赖声明不完整的问题会更早暴露。

这种严格性提高了项目的可维护性:

  • package.json 更接近真实依赖关系;
  • 升级传递依赖时更不容易出现隐性破坏;
  • 新成员可以通过依赖声明理解项目需要什么;
  • 构建环境和本地环境之间更容易保持一致。

如果某个旧工具确实依赖扁平结构,可以使用:

nodeLinker: hoisted

但这会牺牲一部分隔离性,应当把它当作兼容性选项,而不是默认方案。

3. 安装速度:缓存命中后尤其明显

pnpm 的安装速度优势主要来自两个方面:

  1. 依赖包在全局 store 中已经存在时,不需要重复下载;
  2. 项目安装主要进行链接,而不是把所有文件再次复制到项目目录。

实际速度受网络、缓存命中率、依赖数量、磁盘和杀毒软件影响。官方 benchmark 在部分场景中显示 pnpm 可以比 npm 和 Yarn Classic 更快,但不能把“最高两倍”理解成任何项目都固定快两倍。

常用操作:

pnpm install
pnpm add react
pnpm remove react
pnpm update

在 CI 中,通常使用:

pnpm install --frozen-lockfile

这会按照提交到仓库中的 pnpm-lock.yaml 安装,并在清单和锁文件不一致时失败,而不是悄悄改写锁文件。

4. Workspace:适合 monorepo

pnpm 原生支持 workspace,适合在一个仓库中管理多个应用和共享包:

repo/
├── apps/
│   ├── web/
│   └── api/
├── packages/
│   ├── ui/
│   └── config/
├── package.json
├── pnpm-workspace.yaml
└── pnpm-lock.yaml

常用命令:

pnpm --filter web dev
pnpm --filter api test
pnpm --filter './packages/*' build
pnpm -r lint

Workspace 能力包括:

  • 自动识别仓库中的多个包;
  • 在本地链接 workspace 内部依赖;
  • 使用 workspace: 协议表达内部依赖;
  • 通过 --filter 只操作目标项目及其依赖;
  • 使用 -r 对多个项目递归执行脚本;
  • 使用 catalog 集中维护一组依赖版本;
  • 使用 pnpm deploy 生成部署用目录。

相比单独维护多个 npm 项目,workspace 能减少重复配置,也能让包之间的依赖关系更清晰。

5. Lockfile 和版本固定

pnpm 使用 pnpm-lock.yaml 记录实际解析出的依赖版本、完整依赖图和完整性信息。

团队可以在 package.json 中固定包管理器版本:

{
  "packageManager": "pnpm@11.22.0"
}

固定版本可以减少以下问题:

  • 不同开发者生成不同的 lockfile;
  • CI 和本地解析结果不同;
  • 包管理器自身升级带来意外行为变化;
  • workspace 的安装结果不一致。

迁移到 pnpm 后,应该提交 pnpm-lock.yaml,并尽量不要在同一个项目中交替使用 npm、Yarn 和 pnpm 修改依赖。

6. 命令接近 npm,迁移成本低

pnpm 保留了许多熟悉的命令:

npmpnpm
npm installpnpm install
npm install axiospnpm add axios
npm install -D vitepnpm add -D vite
npm uninstall axiospnpm remove axios
npm run devpnpm run dev 或 pnpm dev
npx vitepnpm dlx vite
npm execpnpm exec

因此,迁移的主要学习成本不是命令,而是理解全局 store、非扁平 node_modules、workspace 和 filter。

7. 需要接受的代价

pnpm 的优势来自它不同于 npm 的存储和链接模型,所以也有相应代价:

  • 一些假设扁平 node_modules 的旧工具可能需要额外配置;
  • Windows 上文件链接可能受到权限、跨磁盘和杀毒软件占用的影响;
  • 团队需要统一包管理器和 lockfile;
  • 第一次迁移可能暴露项目长期隐藏的漏声明依赖;
  • 需要让开发者理解 pnpm store 和 .pnpm 目录,而不是把 node_modules 当作普通复制目录。

这些通常不是 pnpm 的缺陷,而是更严格、更高效的模型与旧生态假设之间的兼容成本。

适合使用 pnpm 的场景

pnpm 特别适合:

  • 同一台电脑上维护多个 Node.js 项目;
  • 大型前端项目;
  • monorepo 和多包仓库;
  • 需要控制磁盘占用的开发环境;
  • 重视 lockfile 和可复现安装的团队;
  • 希望尽早发现未声明依赖的项目。

单个很小的项目使用 npm 也完全合理;pnpm 的收益会随着项目数量、依赖规模和 workspace 复杂度增加而变得更明显。

迁移的最小流程

从已有 npm 项目迁移时,可以在分支或项目副本中操作:

pnpm import
pnpm install
pnpm test
pnpm build

pnpm import 可用于从已有的 package-lock.json 或 yarn.lock 导入依赖信息。确认测试和构建通过后,提交 pnpm-lock.yaml,并移除团队不再使用的旧 lockfile,避免后续工具选择错误。

总结

pnpm 的主要收益可以概括为:

  1. 用内容寻址存储和链接机制减少重复文件;
  2. 用严格依赖布局减少幽灵依赖;
  3. 用全局缓存和链接提升安装效率;
  4. 用 workspace、filter 和 catalog 管理 monorepo;
  5. 用 lockfile 和版本固定提高可复现性。

如果只看命令,pnpm 和 npm 很像;如果看底层模型,pnpm 更像是一套针对现代 Node.js 多项目开发重新设计的依赖管理系统。

参考


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/nodejs