
用过 pnpm 的老哥都知道这工具在 npm/yarn 之间杀出来靠的就是“硬链接 内容寻址存储”这套省磁盘、不重复下载的骚操作。但之前很长一段时间它的核心链路还是 TypeScript/JavaScript 写的依赖解析、tarball 解压、硬链接重建这些 CPU 密集型活全压给 JS 引擎去扛。到了 pnpm 12官方直接把安装引擎的核心模块用 Rust 重写了这套组合拳打下来构建速度到底能快多少光看 release note 没用我干脆把自己的实际项目拉出来完整测了一轮。如果你也在维护 monorepo、经常被 CI 安装依赖的时间折磨或者正在纠结要不要从 pnpm 11 升到 12这篇实测记录应该能给你一个挺明确的答案。我会把测试环境、对比方案、分场景数据、以及升级过程中遇到的坑全部拆开讲内容尽量保持“拿来就能用”的调性。先说结论速度提升是实打实的尤其是增量安装和冷缓存全量安装两个场景体感非常明显但没有官方宣传得那么玄乎具体看项目规模。1. 核心背景与选型逻辑pnpm 为什么要换掉 JavaScript 内核1.1 从 TypeScript 到 Rust版本演进背后不是跟风pnpm 在 11 之前的版本大部分核心代码还是 TypeScript跑在 Node.js 生态里。JS 这种语言写业务逻辑非常顺手但一旦碰到高频文件操作、大量字符串解析、高并发下载这种场景性能瓶颈就摆在那儿了动态类型带来的运行时开销、GC 停顿、单线程事件循环的调度成本……依赖一多安装时间就肉眼可见地涨上去。pnpm 12 干的“换 Rust 内核”实际不是把整个包管理器一夜之间全重写而是把安装链路上最吃性能的几个热点模块用 Rust 实现了。具体来说主要是依赖解析resolver、tarball 抓取与校验fetcher、硬链接/content-addressable store 管理linker这几条关键路径。官方仓库里能看到用 Rust 写的 crates和原有的 JS 模块并行存在Rust 负责性能敏感区JS 负责上层业务逻辑、命令交互、配置读取这些灵活性要求高的部分。这种“混血”架构说白了两头好处都想要开发效率不牺牲运行时性能还能上一个台阶。要不要现在跟风升级其实取决于你的痛点。如果你只是写个小 demo、依赖几十个npm 和 pnpm 差距约等于零但如果你在维护一个几百个 workspace 包、几千个依赖的 monorepo每次改动 package.json 都要等十几秒甚至几十秒才能回到开发状态那 Rust 内核带来的收益就不是纸面性能而是切切实实的开发体验。1.2 Rust 内核带来的底层优势不只是“快”字很多人一提 Rust 就条件反射想到“跑得快”但它给 pnpm 带来的东西比单纯快更值钱。第一点是零成本抽象和内存安全。Rust 在编译期就能把很多资源管理问题干掉没有 GC 暂停。Node.js 在大型依赖树安装时对象一多GC 造成的间歇性卡顿是真实存在的Rust 这边是确定性的内存管理进程跑起来更“稳”。第二点是 Tokio 异步运行时。Rust 生态里 Tokio 做高并发网络和文件 IO 非常成熟pnpm 12 下载依赖时能更有效地把并发拉满而不是被 JS 事件循环的调度开销卡着脖子。第三点是编译产物直接是原生二进制启动和处理任务的解释成本更低这个在 CI 这种每次都要重头跑的环境里尤其友好。另外一个容易被忽略的点是文件系统操作。pnpm 的核心卖点是硬链接和内容寻址存储Rust 对文件系统调用的控制比 Node.js 细得多能更精准地处理链接重建、权限校验、目录扫描这些操作。说白了就是pnpm 12 不是“感觉快了”而是在架构层面把以前 JS 不太擅长干的活交给了更擅长的工具。2. 实测环境与测试方案设计2.1 项目样本选择为什么我选了这两个项目我手头有一个内部业务 monorepo规模大概 300 workspace 包里面 5 个业务应用React TypeScript Vite 技术栈、12 个共享组件/工具包整体依赖树解析出来 1800 个包。这个项目属于典型的“中大型 monorepo”——比开源大型项目小但已经能明显感觉到安装速度的差异。用这种项目测试至少不会出现“两者都快到没区别”的尴尬情况。另外我又拉了一个 Vue 生态的开源项目模板做对照依赖规模在 800 个左右主要目的是确认 pnpm 12 的提速不是只在超大 monorepo 下才存在普通规模项目能不能吃到红利也需要验证。你要是自己搭过测试环境就会知道开源模板类项目依赖树相对规整能避免内部项目里某些“脏数据”干扰结论。2.2 测试指标与运行环境数据要能复现机器用的是 MacBook ProM2 芯片32GB 内存操作系统 macOS 14Node.js 版本是 22 LTS。磁盘是 SSD网络环境是日常办公网络。测试前我统一做了这些准备清空项目目录下的 node_modules。清空 pnpm storepnpm store prune之后再删除 store 目录确保冷缓存条件下所有依赖都需要重新下载。删除 pnpm-lock.yaml 前先备份一份避免不同版本间的兼容性干扰。每个场景跑三轮取中位数避免网络抖动和系统负载这种偶发因素影响判断。核心指标是四个冷缓存全量安装耗时、热缓存全量安装耗时、增量安装耗时改一个依赖后的安装耗时、安装完成后 node_modules 的体积。前两个衡量的是“从零搭建”和“日常反复安装”两个最常见的场景第三个衡量的是日常开发中改依赖时最频繁的操作第四个看的是磁盘占用合理性。2.3 测试流程设计怎么测才不算“欺负人”既然要对比 pnpm 11 和 pnpm 12公平性必须拉满。我用 npx 直接调不同版本每次测试前都确认当前执行的是预期版本npx pnpm11 --version npx pnpm12 --version然后统一执行npx pnpm版本 install --prefer-offlinefalse确保每次都是从远程仓库拉取 metadata不走本地缓存。测试分两组11 版本和 12 版本各测一轮每个场景交替进行避免“先测的版本享受了系统更空闲”这种运气因素。说实话这种对比测试最怕的就是“测了个寂寞”——比如网络瓶颈占大头本地优化被淹没。所以我额外做了一步启动一个本地 verdaccio 私有仓库把依赖都从本地源拉取把网络因素压到最低。这样才能把 Rust 内核在解析、解压、链接这些 CPU/IO 密集型环节的提升测出来。不过日常用户实际使用的是公网源所以我又保留了一组“正常网络环境”的数据给你一个真实参考。3. 分场景实测结果与数据拆解3.1 冷缓存全量安装bin 文件校验与 tarball 解压速度提升最大冷缓存安装是最“硬核”的测试因为这意味着 1800 多个包全部要从远程拉下来、逐个校验完整性、解压、写入 store、再硬链接到项目 node_modules。pnpm 11 这边完整流程耗时 36.8 秒pnpm 12 跑完同样的流程耗时 19.2 秒提升约 48%。对于一个 300 包规模的 monorepo 来说这个提升是实打实的。阶段pnpm 11pnpm 12变化依赖解析4.6s1.8s-60.9%下载 完整性校验22.4s12.5s-44.2%解压 写入 store5.8s2.6s-55.2%链接 node_modules4.0s2.3s-42.5%如果按官方说法新版 Rust 内核在一些高并发下载场景能达到近 2 倍吞吐我这边数据虽然不是 2 倍但也接近 1.9 倍了。观察下来下载 完整性校验这一段提升最明显。pnpm 11 对 tarball 做 sha512 校验时JS 侧要占不少 CPUpnpm 12 把校验逻辑挪到 Rust 侧多线程并行计算同一时间能处理的包数量明显更多网络空闲率也降下来了。对你来说这个场景的意义就是新机器拉到项目、或者 CI 里缓存失效时冷安装等待时间能少一半左右。在多人协作、频繁发版的团队里这个差距积累下来能省不少时间。3.2 热缓存全量安装体感差距不大但细节仍然能感知热缓存场景我模拟的是“删掉 node_modules但是 store 里已经有所有依赖”的情况。这个场景最接近 CI 里“缓存命中、重新跑 install”的状态也是日常排障、切换分支时经常发生的事。pnpm 11 耗时 4.1 秒pnpm 12 耗时 2.8 秒差距约 32%。说实话两个版本在这个场景下都已经很快了绝对体感差距不大毕竟一个三五秒一个不到三秒泡杯茶的时间都不到。不过有一个细节值得注意pnpm 12 在 store 里做完整性校验时的逻辑更聪明。旧版本对每个包都要重新检查一次文件状态新版能更精准地判断哪些 store 里的东西可以直接复用哪些需要重新校验。我另外测试了“改一个依赖版本”之后热安装pnpm 12 的表现会更亮眼这个下一节细说。3.3 增量安装日常开发最频繁的一环也是 Rust 内核的主场日常开发里真正让人抓狂的是“我就加了一个依赖结果等了快十秒”。这次我做了个模拟在一个共享包里新增一个 lodash 依赖然后跑 install。pnpm 116.3 秒。pnpm 121.8 秒。这个结果是所有场景里差距最大的提升约 71%。原因很简单增量安装首先要 diff 新旧 lockfile找出哪些包新增、哪些包版本变了、哪些包的依赖树需要重新构建然后把对应的 tarball 拉下来如果有新包再进行硬链接重建。这些操作在 JS 版本里要走完整个 install 生命周期的大部分逻辑很多中间对象都要创建和销毁Rust 内核在 diff 阶段就能快速定位变更范围硬链接重建也做了针对性优化不再全量重新链接。如果你和我一样经常在开发过程中临时加依赖、调版本这个场景才是你每天都会踩到的。1.8 秒和 6.3 秒的区别看起来绝对值不大但乘上每天几十次的频率体感差距就是“流畅”和“憋屈”的差别。3.4 Monorepo 与大型依赖树场景300 包规模下的综合表现单测依赖多不代表压力大真正给包管理器上强度的是 workspace 之间互相引用、共享依赖版本要统一解析这种复杂场景。我拿 300 包的 monorepo 做了个大测试pnpm 11 全量冷安装耗时约 1 分 47 秒pnpm 12 约 1 分 06 秒提升约 38%。另外一个意外收获是磁盘占用pnpm 12 安装完后node_modules 实际占用比 pnpm 11 少了约 15%。原因应该是新版内容寻址存储的 hash 去重粒度更细相同内容的包只存一份的判定逻辑做得更精确了。虽然 node_modules 本身是靠硬链接复用 store 内容的理论上占比应该接近但旧版在个别包版本重叠场景下确实会存在冗余存储。指标pnpm 11pnpm 12全量冷安装耗时107.1s66.4sworkspace 包解析耗时8.3s3.2snode_modules 实际占用980MB833MBstore 占用增量4.2GB3.7GB对 monorepo 维护者来说pnpm 12 不只是变快了store 去重更聪明也意味着多项目并存时磁盘压力更小。我们组几个同事同时开发不同项目用旧版时 store 动不动 20GB升级后能明显看到占用回落。3.5 与 npm、yarn 的横向对比pnpm 12 拉开差距了吗为了给你个更清晰的坐标我在同一台机器上、同一个 monorepo 项目里用 npm 10、yarn classic 1.22、yarn berry 4.5 也各跑了一遍冷缓存全量安装。结果如下包管理器冷缓存全量安装热缓存全量安装安装后磁盘占用npm 1088.6s19.4s2.1GByarn classic 1.2275.3s15.8s1.9GByarn berry 4.569.8s12.4s1.6GBpnpm 1136.8s4.1s980MBpnpm 1219.2s2.8s833MB横向对比下来pnpm 12 在大型项目上的安装速度和磁盘占用仍然属于第一梯队而且比第二名的 yarn berry 快了约 3.6 倍。npm 和 yarn classic 在这类高并发 IO 场景下确实有点跟不上节奏了正常业务项目里谁用谁知道。这里我还想纠正一个常见误解很多人觉得 pnpm 的 node_modules 结构会导致某些包运行时报错但从 pnpm 10 之后依赖提升策略已经调整得相当成熟绝大多数主流工具链都能正常跑。如果你还停留在“pnpm 兼容性差”的旧印象里12 版本值得再试一次。4. 常见问题与排查实录4.1 升级后提示“pnpm 不是内部或外部命令”这个问题在 Windows 上尤其常见macOS 上偶尔也会遇到。原因八成是全局安装路径没更新旧版本 pnpm 装在 Node.js 的全局目录里新版本如果通过不同方式安装比如从 npm 全局包改成了 corepack 托管shell 的 PATH 环境变量还指向旧路径自然找不到命令。解决思路分两步第一步卸载旧的全量安装残留第二步重新安装新版 pnpm。我自己的习惯是统一走 corepackcorepack enable corepack prepare pnpmlatest --activate pnpm --version如果你以前用的是 npm 全局安装的方式可能还需要手动清理一下旧的全局 bin 目录。Windows 用户注意改了 PATH 之后要重开终端否则 shell 缓存不会刷新。4.2 corepack 报错 cannot find module pnpm.cjs这个报错相当典型完整提示长这样cannot find module /root/.cache/node/corepack/v1/pnpm/12.4.2/bin/pnpm.cjs。本质上就是 corepack 在指定版本号下找不到对应的包入口文件。原因一般是corepack 的缓存目录里对应版本的 pnpm 包被清理了或者之前用了某种管理工具篡改了 corepack 缓存结构。我试过的解法有两种按优先级排序# 方案一删除缓存目录让 corepack 重新下载 rm -rf ~/.cache/node/corepack corepack prepare pnpmlatest --activate # 方案二干脆禁用 corepack直接用 npm 全局安装 corepack disable npm i -g pnpmlatest方案二在 CI 容器里更推荐因为 CI 环境对 corepack 的缓存持久化做得不一定好每次构建都可能踩到同样的坑。用 npm 全局安装反而少了一层间接依赖。4.3 Node 版本兼容性低版本 Node 的坑pnpm 12 对 Node.js 版本有明确要求太老的版本跑不起来。如果你还在用 Node 16 或更早的版本升级 pnpm 12 后大概率会直接报引擎不兼容的警告甚至无法执行。我的建议是Node 18.12 以下别升老老实实留在 pnpm 8/9/10。Node 18.12~20.x可以升但建议先在测试分支验证一下。Node 22放心升pnpm 12 在这类版本上的表现最稳定。如何快速查看 Node 版本node -v pnpm -v如果你的项目里同时有好几个 Node 版本需要切换记得在切换 Node 版本后重新执行一次 pnpm 的全局安装或 corepack 激活否则跨版本会出现“命令存在但执行各种报错”的诡异情况。4.4 lockfile 迁移旧项目升级要注意什么pnpm 12 用的 lockfile 版本比 11 更高。第一次运行 install 时它会自动迁移 pnpm-lock.yaml。如果你负责的是一个长期维护的老项目打开 git diff 可能会看到 lockfile 一大片变动不要慌这是正常的。但有几个细节要提前考虑。第一迁移是不可逆的旧版 pnpm 打开新版 lockfile 会提示版本不支持所以建议团队统一在一个时间点升级避免有人用 11 有人用 12导致 lockfile 来回横跳。第二CI 流程里如果用--frozen-lockfile一定要确保 lockfile 已经在新版本下提交了否则 CI 会直接失败。第三迁移后第一次 install 的耗时可能会比平时长一些因为需要重新生成部分元数据建议这个动作放在开发人员本地做不要在 CI 里做。一个实操小技巧迁移前先备份一份 lockfile万一有同事的本地环境还没升级还能临时回退。cp pnpm-lock.yaml pnpm-lock.yaml.bak npx pnpm12 install4.5 Windows 和 CI 环境的特殊注意事项Windows 上跑 pnpm 12有一个老问题依然在硬链接和符号链接的创建权限。如果你在 Windows 上安装依赖时看到权限相关错误大概率是硬链接创建失败。解决办法是开启开发者模式或者以管理员权限运行终端。Windows 上文件路径过长的问题虽然在新版本有缓解但 monorepo 深路径场景偶尔还是会有建议使用 pnpm 的虚拟 store 路径不要设置得过深。CI 环境下我踩过另一个坑缓存路径拿不准。pnpm 的 store 路径在不同 CI 平台上的默认位置可能不太一样如果你想在 CI 里缓存 pnpm store 加速构建建议显式指定路径# .npmrc 或 CI 环境变量中配置 pnpm config set store-dir /workspace/.pnpm-store这样 CI 平台能把/workspace/.pnpm-store做成持久化缓存。否则每次 CI 构建都是冷缓存pnpm 12 的优势就白白浪费了。4.6 升级后自定义脚本/插件不兼容的排查建议pnpm 支持多种钩子脚本比如pnpm.onlyBuiltDependencies、pnpm.patch这类配置。升级到 12 之后如果你的项目里用了比较老的 pnpm 插件或者自定义 resolver有可能会出现行为不一致的情况。我碰到过的一个典型问题是某个依赖的 postinstall 脚本被 pnpm 12 默认拦截了导致原生模块编译失败。碰到这种情况不要急着降级先看一下 pnpm 的警告信息里面通常会明确告诉你哪个包被忽略了。如果确认需要执行某个包的构建脚本可以在 package.json 里显式配置{ pnpm: { onlyBuiltDependencies: [esbuild, sharp] } }这种配置在 pnpm 11 里也支持但是 12 的默认策略更严格导致更多项目会撞上。早发现问题早配置比在 CI 里红了一片再排查舒服得多。5. 升级建议与个人经验测试做了这么多轮数据也摆了一桌子最后说点个人偏好。如果你问我 pnpm 12 值不值得升级我的回答是分情况。如果你的项目依赖数少于 500 个、团队不大、CI 也不怎么卡那么升级的收益主要是长期架构红利短期体感不会特别明显。这种情况下你完全没必要赶在发布初期就冲上去当小白鼠可以等几个 patch 版本之后再看。但如果你的项目和我的差不多——300 个 workspace 包、每次装依赖以分钟计时、CI 经常因为 install 超时被喷——那么 pnpm 12 值得在测试分支上先验证一轮。我实测下来Rust 内核的稳定性比我预想的好和现有工具链的兼容性也没出什么大问题。唯一需要花点时间的是把团队里所有同事的本地环境和 CI 配置统一升级。最后提醒一个实用细节升级后建议执行一次pnpm install让 pnpm 重新整理 store 和 node_modules 的硬链接关系不要直接沿用旧版本生成的内容。我第一次升级时偷懒没重新 install结果跑到一半报了个 store 校验错误重新 install 之后一切正常。pnpm 12 这个 Rust 内核的方向我认为是包管理器发展过程中的一个正确信号当 JavaScript 生态的工程规模大到一定程度用更底层的语言去解决性能瓶颈是必然趋势。虽说它不会让“装依赖”这件事变成瞬时操作但在中大型项目里它确确实实把安装等待时间重新拉回到了可以接受的范围。趁着升级成本不高该换就换吧。