pnpm(全称 Performant npm,或 Performant Node Package Manager)是一款面向 Node.js 运行时的 JavaScript 包管理器,由 Zoltan Kochan 于 2016 年发起、2017 年正式发布。它通过内容可寻址存储(Content-Addressable Storage, CAS)与硬链接、符号链接相结合的非扁平化 node_modules 结构,实现依赖的一次下载、多处复用,在磁盘空间占用和安装速度上相较传统方案有显著优化。pnpm 同时提供严格的依赖隔离机制,从根本上避免幽灵依赖(phantom dependency)问题,并内置 monorepo 工作区支持,已被 Next.js、Vite、Vue、Prisma 等大量开源项目采用。
内容可寻址存储是一种通过文件内容本身生成的哈希值来命名和访问数据的存储方式。pnpm 使用 SHA-512 哈希算法对每个文件的完整内容进行摘要,以哈希值作为文件在全局 store 中的唯一标识。相同内容的文件无论来自哪个包、出现在哪个项目中,都会生成相同的哈希值,因此只在全局 store 中存储一份。
pnpm 在用户目录下维护一个全局存储目录,默认位置因操作系统而异:Linux 为 ~/.local/share/pnpm/store,macOS 为 ~/Library/pnpm/store,Windows 为 %LOCALAPPDATA%\pnpm\store。存储目录内部按哈希值的前两位字符划分为 256 个子目录(00 至 ff),每个子目录内存放对应哈希前缀的所有文件。文件根据权限属性被区分为可执行文件、不可执行文件和索引文件三类,分别附加不同的路径后缀。pnpm v11 起,store 目录采用版本化命名(如 v11),由 STORE_VERSION 常量控制,底层使用 SQLite 索引提升性能。
当包版本升级时,pnpm 只会向 store 中新增有变化的文件,未改动的文件直接复用已有存储。例如某个包包含 100 个文件,新版本仅修改其中 1 个文件,则 store 中只新增这 1 个文件,其余 99 个文件继续共享。这种机制使得 pnpm 在频繁更新依赖的场景下仍能保持较低的磁盘开销和较快的安装速度。
硬链接是操作系统层面的文件别名机制,多个目录项指向磁盘上的同一块数据(同一 inode)。pnpm 在安装依赖时,将全局 store 中的文件通过硬链接引入项目的 node_modules,而非复制文件内容。由于硬链接不占用额外磁盘空间,且删除任一链接不影响其他链接指向的原始数据,因此多个项目共享同一版本依赖时,磁盘上实际只存在一份物理文件。
在硬链接的基础上,pnpm 使用符号链接(symlink)搭建项目的依赖层级结构。项目的 node_modules 根目录下仅存放直接依赖的符号链接,间接依赖则被隔离在 .pnpm 虚拟存储目录中。符号链接指向 .pnpm 内对应包的实际位置,而 .pnpm 内的文件又通过硬链接指向全局 store。这种双层链接结构使得依赖图再深,文件系统的目录深度始终保持恒定。
传统包管理器(npm、Yarn Classic)采用扁平化提升策略,每个项目都在各自的 node_modules 中复制完整的依赖副本。若 100 个项目使用同一版本的 lodash,磁盘上便存在 100 份 lodash。pnpm 通过全局 store 与硬链接机制,使这 100 个项目共享同一份物理文件,实测可节省 50%~75% 的磁盘空间。
pnpm 的存储体系由两层构成:底层是内容寻址存储(CAS),保存所有已下载文件的物理副本;上层是虚拟存储(Virtual Store),为每个项目维护一套符合 Node.js 模块解析算法的目录结构。默认情况下,每个项目在 node_modules/.pnpm 中拥有独立的虚拟存储投影,虚拟存储内的文件通过硬链接指向 CAS。pnpm v11 起,全局虚拟存储(Global Virtual Store)对 pnpm dlx 和全局安装的包默认启用;普通项目如需启用,可在配置文件中设置 enableGlobalVirtualStore: true。全局虚拟存储可将多个项目的虚拟存储合并为单一共享位置,进一步减少重复的目录结构开销。
store 目录采用版本化命名(如 v10、v11),由 STORE_VERSION 常量控制。这种设计确保存储格式变更时能够平滑迁移,不同版本的 pnpm 可以识别并兼容对应格式的 store。用户可通过 pnpm store path 命令查询当前激活的 store 路径,通过 pnpm store status 检查 store 完整性,通过 pnpm store prune 清理未被引用的孤立包。
pnpm v11 起,全局安装的包(pnpm add -g)采用隔离安装模式。每个全局安装组拥有独立的目录,包含自己的 package.json、pnpm-lock.yaml 和 node_modules,组与组之间互不干扰。二进制文件的 shim 统一放置在 {pnpmHomeDir}/bin/ 中,指向对应安装组的 node_modules。
pnpm 允许同一依赖的多个版本在同一项目中并存,每个版本作为独立的包条目存在于 .pnpm 虚拟存储中。当不同层级的依赖要求同一包的不同版本时,pnpm 会为每个版本创建单独的目录条目,通过符号链接分别指向对应的硬链接副本。由于硬链接指向的是 CAS 中按内容哈希区分的文件,不同版本之间不会相互覆盖。
pnpm 在依赖解析阶段会构建完整的依赖树,识别出所有需要的版本及其约束关系。若存在版本冲突(如包 A 要求 lodash@^4.17.0,包 B 要求 lodash@^4.18.0),pnpm 会尝试找到满足所有约束的最高兼容版本;若无法找到,则两个版本并存,各自安装到独立的目录中。这种策略确保了每个依赖都能在其声明的版本范围内正确解析,避免因版本强制统一而引入破坏性变更。
npm 和 Yarn Classic 的扁平化结构会将尽可能多的依赖提升到 node_modules 根目录,当多个包要求同一依赖的不兼容版本时,往往只能保留一个版本,可能导致其他包运行异常。pnpm 的非扁平结构天然支持多版本共存,每个包只能访问其显式声明的依赖版本,从根本上避免了版本冲突引发的隐蔽问题。
pnpm 的 node_modules 采用非扁平化布局:根目录下仅通过符号链接暴露项目在 package.json 中显式声明的直接依赖,所有间接依赖(传递依赖)都被封装在 .pnpm 虚拟存储目录内。这种结构确保项目代码只能 require 或 import 已在 package.json 中登记的包,无法访问未经声明的传递依赖。
默认情况下,pnpm 采用半严格模式(semistrict):node_modules 内部的包可以访问通过提升规则允许的未声明依赖,但 node_modules 外部的代码无法访问其中的内容。若需要更严格的隔离,可在 .npmrc 中设置 hoist=false,完全禁止依赖提升,此时只有显式声明的依赖才可访问。对于绝大多数现代项目,半严格模式已在保证兼容性的同时提供了足够的隔离性。
严格隔离机制会迫使开发者将实际使用的依赖显式写入 package.json。当代码中尝试引用未声明的包时,运行时会立即抛出 Cannot find module 错误,而非像传统方案那样因提升机制而"恰好可用"。这种即时反馈帮助团队在开发阶段就发现依赖声明遗漏,避免生产环境中因传递依赖升级或移除而导致的隐蔽故障。
pnpm-lock.yaml 是 pnpm 的锁文件,记录项目中每个依赖的精确解析结果,包括实际安装的版本号、完整性校验哈希(integrity)、依赖关系图以及 patch 信息。该文件是确定性安装的来源:无论何时何地执行 pnpm install,只要 package.json 和 pnpm-lock.yaml 未被修改,安装的依赖树就完全一致。
锁文件中的每个包条目都包含基于 SHA-512 的完整性哈希值。安装时,pnpm 会校验下载内容的哈希是否与锁文件中记录的一致,防止内容被篡改或传输损坏。对于使用 patch 修改过的依赖,锁文件还会额外记录 patch 文件的哈希,确保修改后的包内容同样可验证。
当 pnpm 检测到运行环境为 CI 时,会自动进入冻结锁文件模式(frozen-lockfile),拒绝任何对锁文件的修改。若 pnpm-lock.yaml 与 package.json 中的依赖声明不匹配,安装将直接失败并报错。这一机制杜绝了"本地能跑、CI 挂掉"的锁文件漂移问题,确保构建环境的可复现性。用户也可通过 pnpm install --frozen-lockfile 手动启用该模式。
幽灵依赖是指项目代码能够访问但未在 package.json 中显式声明的依赖。其成因在于 npm 和 Yarn Classic 采用的扁平化提升策略:为了减少目录嵌套深度,包管理器会将大量传递依赖提升到 node_modules 根目录,使得项目代码可以"顺便"引用这些本不应直接访问的包。这种机制在开发阶段看似便利,实则埋下了不确定性隐患——当传递依赖在新版本中移除或变更 API 时,依赖它的代码会突然失效。
pnpm 通过非扁平化的符号链接结构从根本上切断幽灵依赖的访问路径。项目的 node_modules 根目录只包含直接依赖的符号链接,传递依赖被隔离在 .pnpm 目录深处,Node.js 的模块解析算法无法直接触及。即使开发者尝试 require 一个未声明的包,也会因模块不存在而立即报错。
从 npm 或 Yarn 迁移到 pnpm 时,若项目中存在幽灵依赖,首次 pnpm install 后运行代码会触发 Cannot find module 错误。此时的处理方式是将该包显式添加到 package.json 中(pnpm add ),或确认该引用确实不必要后移除相关代码。对于少数依赖扁平化结构的遗留工具(如某些旧版 Webpack 配置或 React Native 项目),可通过 .npmrc 中的 shamefully-hoist=true 或 public-hoist-pattern[] 配置临时恢复扁平结构,但这会重新引入幽灵依赖风险,仅建议作为迁移过渡方案使用。
由于所有依赖只在全局 store 中存储一份,当多个项目或同一项目的多次安装涉及相同版本的依赖时,pnpm 无需重新下载,只需从 store 中硬链接到目标位置。硬链接是文件系统层面的元数据操作,耗时通常在微秒级;而传统方案需要复制大量小文件,属于 I/O 密集型操作,尤其在 Windows 系统上耗时显著。
pnpm 在安装过程中对依赖的解析、下载和链接操作进行了并行化优化。对于已缓存的包,安装过程几乎完全由硬链接操作构成,无需解压或复制文件内容。官方基准测试显示,在依赖数量较多的应用中,pnpm 的安装速度可达 npm 的 2 倍以上。
pnpm v11 起默认启用的全局虚拟存储(针对 pnpm dlx 和全局安装)进一步减少了重复的目录结构创建开销。在拥有多个项目或频繁切换分支的开发环境中,任何已安装过的依赖版本都可被即时复用,第二次及以后的安装速度接近瞬时完成。
npm 和 Yarn Classic 默认采用扁平化提升策略,将尽可能多的依赖(包括传递依赖)提升到 node_modules 根目录。这种设计的初衷是减少目录嵌套深度,兼容早期 Node.js 的模块解析行为。其副作用是项目代码可以访问任意层级的依赖,形成幽灵依赖,同时同一依赖的多个版本无法并存,可能引发版本冲突。
pnpm 默认将依赖提升到 node_modules/.pnpm/node_modules(隐藏目录),而非根目录。这意味着依赖对 node_modules 内部的包可见,但对外部代码不可见,在兼容性与隔离性之间取得平衡。用户可通过 .npmrc 配置灵活调整提升行为:hoist=false 完全禁止提升,实现最严格隔离;public-hoist-pattern[] 将匹配模式的包提升到根目录,用于兼容特定工具;shamefully-hoist=true 则将所有依赖提升到根目录,行为等同于 npm。
大多数现代工具(Vite、Webpack 5、TypeScript 等)已兼容 pnpm 的非扁平结构。少数依赖扁平化结构的工具(如 React Native 的 Metro 打包器、部分 Webpack 4 配置、某些 serverless 部署环境)需要借助 node-linker=hoisted 配置生成无符号链接的扁平 node_modules,此时会失去幽灵依赖保护,但保留 pnpm 的 store 和锁文件优势。
pnpm 通过 pnpm-workspace.yaml 文件声明工作区范围,使用 glob 模式匹配子包目录。例如配置 packages: ['packages/*', 'apps/*'] 后,所有匹配目录中包含 package.json 的文件夹都会成为工作区成员。根目录的 package.json 可定义工作区级脚本和共享开发依赖。
工作区内的包之间可通过 workspace: 协议相互依赖,如 "@myorg/utils": "workspace:*"。该协议确保开发期间始终使用本地工作区的最新版本,发布时自动替换为实际版本号。pnpm 还支持 workspace:^ 和 workspace:~ 等变体,用于指定兼容版本范围。
pnpm 的 --filter(简写 -F)参数提供强大的包筛选能力,支持按名称、路径、依赖关系和 Git 变更检测进行精准操作。常用语法包括:-F @scope/pkg 选中指定包,-F pkg... 选中包及其所有依赖,-F ...pkg 选中包及其所有依赖者,-F "...[origin/main]" 选中自指定分支以来有变更的包。配合 -r(recursive)参数可在所有工作区中批量执行命令,--parallel 可并行运行以加速构建和测试。
pnpm v10 起引入安全机制,默认禁用所有依赖的生命周期脚本(preinstall、install、postinstall 等)。当未配置任何构建策略时,pnpm 将 onlyBuiltDependencies 设为空数组,阻止所有包在安装阶段执行脚本,从而防范通过恶意 postinstall 脚本发起的供应链攻击。
若项目确实需要运行特定包的构建脚本(如 esbuild、core-js、原生模块编译等),可在 pnpm-workspace.yaml 中通过 allowBuilds 字段显式声明允许的包名。pnpm v10.26.0 起引入的 allowBuilds 字段提供了更细粒度的控制,支持按包名和版本范围分别设置允许或拒绝;pnpm v11 起,onlyBuiltDependencies 等旧字段已被 allowBuilds 取代。首次安装时,pnpm 会交互式提示用户确认需要运行脚本的包,并将选择写入配置文件。
当 strictDepBuilds 设为 true 时,若发现未审核的构建脚本,安装将直接以非零退出码失败,而非仅打印警告。此外,onlyBuiltDependencies 支持精确版本锁定(如 esbuild@0.25.1)和版本范围(如 nx@21.6.4 || 21.6.5),确保只有指定版本的包能够执行脚本,防止通过版本升级引入未授权脚本执行。
pnpm 支持 Windows、Linux 和 macOS 三大主流操作系统。在 Linux 上,同时提供基于 glibc 和 musl 的构建版本,Alpine 等基于 musl 的发行版可自动选择对应版本。独立安装脚本无需预装 Node.js,pnpm 本身是自包含的可执行文件,安装后可通过 pnpm runtime set node lts -g 自动安装所需的 Node.js 运行时。
使用 npm 或 Corepack 安装 pnpm 时,pnpm v10 需要 Node.js v18.12 或更高版本;pnpm v11 起要求 Node.js v22 或更高版本,且以纯 ESM 模块分发。对于不支持独立安装脚本的环境(如 Intel 芯片的 macOS),可通过 npm、Corepack 或 Homebrew 安装。
pnpm 在主流 CI 平台(GitHub Actions、GitLab CI、Azure Pipelines、AppVeyor 等)上均有官方支持或成熟集成方案。GitHub Actions 提供 pnpm/action-setup 官方 Action,其他平台可通过独立安装脚本或 Corepack 启用。pnpm 会自动检测 CI 环境并启用冻结锁文件模式,确保构建的可复现性。
pnpm 内置补丁功能,允许开发者在不 fork 仓库的情况下修改第三方依赖的源码。使用 pnpm patch 命令后,pnpm 会将目标包解压到临时目录中,开发者可直接编辑其中的文件。修改完成后,通过 pnpm patch-commit 生成统一差异格式的补丁文件,并自动在 package.json 的 pnpm.patchedDependencies 字段中注册补丁路径。
补丁可按包名、精确版本或版本范围进行匹配。优先级从高到低依次为:精确版本补丁、版本范围补丁、仅包名补丁。例如可为 foo@2.1.0 指定专用补丁,为 foo@^2.0.0 指定通用补丁,未匹配到更具体版本的包将应用最宽泛的补丁。锁文件中会记录补丁文件的哈希值,确保补丁内容未被篡改。
补丁在安装阶段自动应用,无需额外操作。若需移除补丁,使用 pnpm patch-remove 命令即可清除补丁文件和相关配置。pnpm v11 起,补丁应用失败会直接抛出错误,不再提供忽略失败的选项,以确保补丁机制的可靠性。
pnpm 沿用 npm 的 registry 配置机制,可通过 .npmrc 文件或 pnpm config set registry 命令指定自定义 registry 地址。对于作用域包(scoped package),可单独配置其 registry,如 @myorg:registry=https://private.example.com。pnpm v11.1.0 起支持命名 registry 别名,可通过 pnpm-workspace.yaml 中的 namedRegistries 字段映射特定别名到指定 registry。
pnpm 支持从本地 tarball 文件(.tar、.tar.gz、.tgz)或本地目录安装依赖。从目录安装时,会在 node_modules 中创建符号链接,效果等同于 pnpm link。这种方式适用于本地开发调试尚未发布的包。
pnpm v10.9.0 起支持通过 jsr: 协议前缀从 JSR(jsr.io)注册表安装包,用法与 npm 类似,如 pnpm add jsr:@hono/hono。JSR 是面向 Deno 和 Node.js 的现代化包注册表,pnpm 对其提供了原生集成支持。
overrides 字段允许强制指定依赖图中任意包的版本,包括传递依赖和对等依赖。该字段只能在项目根目录的 pnpm-workspace.yaml 中设置(pnpm v11 起不再读取 package.json 的 pnpm 字段)。例如 "overrides": {"lodash": "^4.17.21"} 会将整个依赖树中的 lodash 统一锁定到指定版本,用于修复安全漏洞或统一版本。
可通过 > 分隔符限定覆盖的作用范围,如 "foo@1>bar": "2" 表示仅覆盖 foo@1 所依赖的 bar,不影响其他包。若需移除某依赖的可选依赖项以减小安装体积,可使用 "-" 作为值,如 "foo@1.0.0>bar": "-" 会从 foo@1.0.0 的依赖中移除 bar。
pnpm v11.13.0 引入收敛覆盖(convergence override),通过空范围选择器(如 "form-data@")仅在依赖声明的版本范围允许时统一版本,避免强制不兼容版本。packageExtensions 字段则用于扩展包的元数据,如为缺少对等依赖声明的包补充 peerDependencies,无需修改包本身即可修复生态中的兼容性问题。
pnpm 支持通过 Git URL、GitHub/GitLab/Bitbucket 简写或 git+ssh 协议从代码仓库安装依赖。可指定分支、标签、提交哈希或 semver 范围,如 pnpm add github:user/repo#v1.0.0 或 pnpm add user/repo#semver:^2.0.0。对于 monorepo 的子目录,可通过 path: 参数仅安装指定子目录。pnpm v11.21 起,对 GitHub、GitLab、Bitbucket 上的仓库统一通过规范 HTTPS URL 解析,锁文件不再记录 SSH URL,确保跨环境的一致性。
pnpm 支持从可访问的 HTTP/HTTPS URL 直接安装 tarball 包,如 pnpm add https://example.com/package.tgz。安装时会下载 tarball、计算哈希并记录到锁文件中。需注意,早期版本的 pnpm 对 HTTP/HTTPS tarball 依赖的完整性哈希记录存在缺陷,可能导致锁文件完整性校验被绕过,建议升级至最新版本以获得完整保护。
git 依赖和远程 tarball 属于"异域来源"(exotic sources),在传递依赖中使用可能引入供应链风险。pnpm 提供 blockExoticSubdeps 配置项,可阻止传递依赖使用异域来源,强制所有间接依赖从可信 registry 解析。对于直接依赖,建议优先使用 npm registry 发布的包而非 git 仓库,以降低风险。
pnpm 的内容寻址存储天然适合作为 CI 缓存对象。与缓存每个项目独立的 node_modules 相比,缓存全局 store 的体积更小(通常可减少 40%~60%),且缓存命中率更高。当多个工作区共享大量相同依赖时,一次缓存恢复即可覆盖所有工作区的依赖需求,显著缩短 CI 安装时间。
CI 环境中 pnpm 自动启用冻结锁文件模式,确保每次构建安装的依赖与本地开发完全一致。配合 --prefer-offline 参数,当 store 缓存已恢复时,pnpm 会跳过网络检查直接从本地缓存链接,进一步压缩安装耗时。实测数据显示,配置了 store 缓存的 CI 流水线,其安装时间可比未缓存场景缩短 60%~70%。
pnpm 支持在 CI 中按变更范围精准安装和构建,如 pnpm --filter "...[origin/main]" build 仅处理自上次主分支更新以来有变更的包及其依赖者,避免全量构建的资源浪费。配合 Turborepo、Nx 等构建编排工具,可实现基于依赖图的增量构建,在大规模 monorepo 中效果尤为显著。
在冷安装(无缓存)场景下,pnpm 通常比 npm 快 2~3 倍,与 Yarn Berry(v4)处于同一梯队。Yarn Classic 的安装速度与 npm 接近,而 Yarn Berry 的 Plug'n'Play(PnP)模式因无需生成 node_modules 目录,在大型项目中的冷安装速度略优于 pnpm。在热安装(有缓存)场景下,pnpm 凭借硬链接机制通常表现最佳。
pnpm 的磁盘效率显著优于 Yarn。Yarn Classic 与 npm 类似,采用扁平化复制策略,磁盘占用与 npm 基本持平。Yarn Berry 的 PnP 模式将依赖打包为 zip 存档,磁盘占用极低(接近 0,因为不展开 node_modules),但需将存档提交到版本控制或维护全局缓存。pnpm 通过内容寻址存储与硬链接,在保持传统 node_modules 结构的同时实现 50%~75% 的磁盘节省,且无需改变工具链的工作方式。
pnpm 的 node_modules 结构对现有工具链的兼容性较好,大多数项目可无缝迁移。Yarn Berry 的 PnP 模式需要工具链显式支持(如通过 .pnp.cjs 文件解析模块),部分遗留工具存在兼容问题,迁移成本较高。若团队已深度使用 Yarn 且对 PnP 的零安装、严格约束等特性有需求,可继续使用 Yarn Berry;若追求磁盘效率和更平滑的迁移体验,pnpm 是更优选择。
pnpm 是专注于依赖管理的包管理器,基于 Node.js 生态构建,核心优势在于内容寻址存储和严格的依赖隔离。Bun 则是一个全栈 JavaScript 运行时,内置包管理器功能,基于 Zig 语言实现原生级性能。Bun 的安装速度极快(冷安装通常比 npm 快 5~20 倍),但采用传统的扁平化 node_modules 结构,不提供 pnpm 的磁盘节省和严格隔离特性。
在冷安装速度上,Bun 凭借原生实现和并行下载策略明显领先,尤其适合对安装耗时敏感的 CI 环境。pnpm 在热安装和磁盘效率上表现更优:一个拥有 10 个项目的开发者通过 pnpm 可节省 15~40 GB 磁盘空间,而 Bun 的磁盘占用与 Yarn 4 相近,不具备内容寻址存储带来的去重优势。
若项目的首要痛点是安装速度和一体化运行时能力,Bun 是更合适的选择;若关注磁盘空间节省、依赖正确性(幽灵依赖防护)以及 monorepo 工具链的成熟度,pnpm 更具优势。两者并非互斥关系:部分团队在开发环境中使用 Bun 进行快速安装,同时保留 Node.js 作为运行时;在需要严格依赖隔离和磁盘效率的场景下切换至 pnpm。对于大多数专业项目,pnpm 在功能完整性和生态兼容性之间提供了更均衡的方案。