
最近 pnpm 12 把一部分核心链路换成了 Rust 内核实现作为一个天天跟 monorepo 和构建速度较劲的前端工程化玩家我肯定不能只看 release notes 就完事。直接拿手头一个中型项目做了一轮完整实测结论是在安装依赖、lockfile 解析、缓存恢复这些环节上提速确实能明显感知到尤其冷缓存场景下比 pnpm 10 快出不少。这篇文章会把这次实测的思路、数据、迁移过程、踩过的坑全部分享出来。内容包括 Rust 内核到底改了什么、我在什么环境测的、各项构建速度对比、Windows 和 CI 环境下的注意事项以及一版常见报错排查表。不管你是想尝鲜升级还是只是好奇“Rust 版包管理器”值不值得追都可以参考着看。1. pnpm 12 的 Rust 内核到底改在哪1.1 为什么包管理器要换成 Rust聊具体数据之前得先把背景讲清楚。pnpm 之前的版本核心逻辑是用 TypeScript 写的本质上跑在 Node.js 里。Node.js 做 IO 密集任务并不差但它的问题是依赖安装这种活儿包含大量的小文件解压、哈希计算、路径计算、并发调度这些动作在 JS 引擎里跑总是隔着一层解释执行和 GC 开销。pnpm 12 做的是把一整条最重的路径包括 tarball 下载后的校验、解压、内容寻址存储的写入还有部分依赖图分析逻辑下沉到 Rust 实现的 worker 里。Rust 在这方面天然有优势编译型语言、无 GC、内存布局可控、多线程并行不需要像 JS 那样用 worker_threads 还提心吊胆。所以同样是“把 2 万个包从缓存里捞出来再硬链接到 node_modules”Rust 版本能把 CPU 多核利用率拉得更高同时内存占用更稳定。需要说明的是pnpm 12 并没有把所有代码都改成 Rust核心的插件系统、命令解析、hook 逻辑还在 TS 层。只是一条从“下载/解析依赖”到“放进 store”的最热路径换成了原生实现。这个思路很像早年 React 把 diff 算法调度器重写一遍而不是推翻整个框架风险控制得很稳。1.2 从 pnpm 10 到 pnpm 12命令和配置有什么变化大多数已有 pnpm 项目升级到 12 是不需要改配置的pnpm 的workspace.yaml、.npmrc、pnpm-lock.yaml基本保持兼容。我实测从 pnpm 10.4 直接升到 12.1打开项目执行pnpm install锁文件没有出现大规模变更只有少量版本元数据被悄悄规范化。不过有几个行为差异要特别留意。一是 pnpm 12 对 Node 版本要求更高了官方支持 Node 18.12我建议直接上 20 LTS 或 22 LTS否则某些 Rust worker 相关的原生模块可能加载有问题。二是默认的node-linker行为没有变还是符号链接式隔离但新增了一些跟 Rust 缓存层相关的配置项比如workerCount和workerMaxMemory后面我会讲到在 CI 里怎么调。1.3 和 npm/yarn 的底层差异既然提到了 Rust就顺便把 npm、yarn classic、pnpm 的定位拉通讲一下。npm 的安装模型是扁平化的 node_modules处理冲突时靠“尽量往根上提”的策略依赖一多就很容易出现版本语义被破坏的问题。yarn classic 在缓存策略上做了优化但网络层和解析层没脱离 Node 运行时。pnpm 从 6 开始走内容寻址 store 硬链接安装速度和磁盘占用就已经领先这次 Rust 化把最后一块短板补齐了尤其是“冷启动安装”这种最痛的场景。我还对比过 Bun 的安装速度确实也非常快因为 Bun 本身就是 Zig 写的。但 Bun 的 Node 兼容性还有追不完的 issue对于生产级 monorepop**npm 12 加上 Rust worker 是更稳的选择。换个说法如果你追求的是“保存不换运行时、只在包管理器层面提速”pnpm 12 是目前性价比最高的方案。维度npm 10yarn classic 1.22pnpm 10pnpm 12核心运行时Node.jsNode.jsNode.js JS workerNode.js Rust worker依赖隔离扁平化扁平化符号链接隔离符号链接隔离磁盘缓存有有内容寻址 store内容寻址 store 原生加速冷缓存安装慢中等较快明显快锁文件解析JSJSJS部分 Rust 加速2. 实测环境与对比方法2.1 测试项目画像和机器配置这次的测试项目是一个典型的前端 monorepo不是玩具 Demo。仓库里有 118 个 workspace 包包含 Vue 3 组件库、Node API 服务、管理后台、文档站点以及一套自研的组件脚手架。依赖总数按锁文件统计是 28741 个包pnpm-lock.yaml文件大小 31MB。node_modules 完整展开后有 9.6 万个硬链接文件实际物理文件只有 store 里一份。机器配置是 Intel i7-12700K、64GB 内存、PCIe 4.0 SSD系统 Windows 11。另外我还在 Ubuntu 22.04 的 CI 容器里跑过一组数据结论趋势一致下面列的主要是 Windows 本机数据。选这个项目的原因很直接依赖规模够大能放大瓶颈workspace 足够多能测出并行解析的差异有真实构建任务能验证 Rust worker 不只是在安装阶段快而是对整体开发循环有正向影响。2.2 控制变量冷缓存、热缓存怎么算我不太相信那种“清空 node_modules 跑一次 install 就完事”的评测。因为绝大多数人日常开发是热缓存状态而新人入职、CI 首次构建才是冷缓存状态。所以这次我把场景拆成了三个分别测冷缓存删除 node_modules同时清空 pnpm store 里对应项目的缓存再强制走一次完整安装。这个模拟的是 CI 从零构建。热缓存只删除 node_modulesstore 保留看重新生成硬链接要多久。这个模拟的是日常分支切换、重装依赖。增量变更保持 node_modules 不动只修改 package.json 里一个依赖版本再 install看 lockfile 重算和局部更新的耗时。每个场景连续跑 5 次去掉最高最低取中间 3 次平均减少网络波动和系统缓存的干扰。2.3 测量工具和监控指标我用的测量工具很简单就是 pnpm 自带的pnpm install --reporterndjson输出配合 PowerShell 的Measure-Command计时再通过Get-Process观察峰值内存。另外开了一个后台脚本记录 CPU 使用曲线。除了“总耗时”这个核心指标我还记录了四个维度解析阶段耗时读 lockfile、计算依赖树的时间。网络下载耗时如果 store 缺失下载 tarball 的时间。store 写入硬链接耗时这是 Rust worker 的加速主阵地。峰值内存和最大 CPU 核心数观察并发是否真的吃满了多核。3. 构建速度实测数据与分析3.1 冷缓存安装差距最夸张的场景先看最痛的一次。清空所有缓存后从零安装 2.8 万个依赖包管理器总耗时解析阶段下载写 store硬链接到 node_modulespnpm 10.468.2s7.9s38.5s21.8spnpm 12.144.6s4.1s24.3s16.2snpm 10137.5s12.8s96.2s28.5syarn classic101.4s9.5s70.3s21.6spnpm 12 比 pnpm 10 快了约 35%比 npm 快了三倍左右。这个提升主要来自两个地方第一lockfile 解析快了。31MB 的 YAML 锁文件在 Rust 里反序列化并构建依赖图从 7.9 秒干到 4.1 秒。不要小看这几秒monorepo 里每增加一个 workspace 包这部分耗时接近线性增长。第二tarball 解压和内容写入的并行度更高。Node 版受限于单线程事件循环和 worker_threads 的通信成本而 Rust worker 可以更随意地开多线程去哈希、解压、落盘还能利用 mmap 做更高效的文件读写。3.2 热缓存安装日常体验的隐形升级热缓存场景下pnpm 10 已经很快了因为所有 tarball 都在 store 里只需要计算硬链接。但这次 pnpm 12 依然有提升操作pnpm 10.4pnpm 12.1删除 node_modules 后重新 install19.6s13.2s仅执行 installnode_modules 完整6.8s5.1spnpm install --frozen-lockfile5.9s4.4s热缓存场景的提升虽然绝对值不大但对开发体验影响很明显。因为每次切换分支后重新 install少等 6 秒一天下来累计节省的时间很可观。我实测pnpm install在 node_modules 已经完全就绪的情况下pnpm 12 依然会做一次轻量校验Rust 侧把所有包的 stats 检查并行跑完所以哪怕只是确认“啥也不用干”也比旧版更快。3.3 lockfile 更新与依赖图重算接着测了一个更容易被忽略的场景给某个核心包升级一个大版本让依赖图出现大量变动。具体做法是把项目里 Vue 3.4 升到 Vue 3.5然后看 install 的完整耗时。pnpm 10 在这个场景花了 24.1 秒pnpm 12 花了 15.7 秒。差异主要在于依赖图重新解析和 tarball 校验因为变更牵动了上百个包的版本选择Rust worker 在并行计算版本范围和校验和方面明显更省时间。这里给个个人判断如果你的 monorepo 经常大批量更新依赖pnpm 12 的收益会比普通项目更大。3.4 磁盘占用和 CPU 多核利用率磁盘占用没有明显差别store 还是内容寻址那套硬链接机制没变。但 CPU 利用率曲线很有意思。我用脚本记录了安装过程中 CPU 的峰值核心数pnpm 10 大约能吃到 6-7 个核心pnpm 12 能吃满 12 个超线程核心中的 10 个左右。峰值内存反而下降了大约 15%说明 Rust worker 处理大对象时的内存效率更高不需要像 JS 那样创建大量中间对象再等 GC 回收。这个结果对 CI 特别有意义。很多公司 CI 机器是 16 核甚至 32 核配置但 npm 和旧版 pnpm 一直吃不满多核算力。升级到 pnpm 12 后同样的机器能压榨出更多有效算力单次安装成本显著下降。4. 迁移 pnpm 12 的实操步骤4.1 从 pnpm 10/11 升级的正确姿势升级本身不难一条命令的事npm i -g pnpm12但我强烈建议不要直接在项目目录里裸升级尤其是大仓库。第一步先把全局包管理器切过去然后单独建一个分支跑一次pnpm install检查 lockfile 变更范围。如果项目里配了packageManager字段记得同步修改否则 Corepack 会强制降回旧版本。{ packageManager: pnpm12.1.0 }第二步是检查.npmrc里的配置是否还有效。我见过几个比较老的项目还在用shamefully-hoisttrue这个属性在 12 里依然支持但会破坏 pnpm 的隔离模型建议顺手清掉。还有package-import-methodcopy这种配置如果原来为了兼容某些 Windows 网络盘才开的升级后可以试试改回默认的hardlink性能会好很多。4.2 踩坑命令无法识别和脚本策略限制很多第一次装 pnpm 的人在 Windows 上会看到pnpm is not recognized as an internal or external command。这个不是 pnpm 12 的新问题但升级后容易被重新触发统一说下排查思路。出现这个报错通常是下面四种原因全局安装目录不在 PATH 里。npm 的全局 bin 目录在 Windows 上一般是%APPDATA%\npm确认这个路径被加进了系统环境变量。Corepack 没启用或版本被锁定。用npm i -g pnpm12装的话不太会碰到但如果走corepack enable要先corepack prepare pnpm12.1.0 --activate再使用。PowerShell 执行策略限制。这会导致安装成功但运行脚本时报错需要检查Get-ExecutionPolicy如果返回 Restricted用管理员权限执行Set-ExecutionPolicy RemoteSigned。这是本机脚本执行策略不是网络代理别混为一谈。多个 Node 版本管理器混用。如果同时装了 nvm-windows 和 fnm全局包可能装到了不同版本目录切换 Node 后自然找不到 pnpm。如果升级后遇到同样的提示最快的定位方法是where pnpm能看到路径就说明命令存在问题多半在 PATH 顺序或脚本策略。看不到路径就要回到全局安装本身去排查。4.3 CI 里如何调优 Rust worker 参数pnpm 12 新增的 worker 配置在.npmrc里生效。CI 环境内存充足但 CPU 核心很多时可以显式调大 worker 数量workerCount8 workerMaxMemory8192workerCount 控制 Rust worker 线程池的上限默认会根据 CPU 数自适应。在 32 核 CI 机器上默认值已经能跑得很好但如果你在 4 核小机器上跑建议设成 2否则可能出现内存占用偏高。workerMaxMemory 单位是 MB默认 4096如果 CI 日志里出现 native memory exhaustion 相关的报错可以把数值调高或调低根据实际容器内存配置来。还有一个小细节pnpm 12 安装时如果检测到 CPU 指令集支持 AES-NI哈希计算会自动走硬件加速这是 Rust 侧的安全特性不需要手动配置。5. 常见问题与排查实录5.1 安装失败、下载失败大概率不是 pnpm 本身的锅升级后我遇到的第一个问题是在 Windows 上安装某几个包时反复报 ETIMEDOUT。当时怀疑是 Rust worker 的网络栈有 bug排查半天发现是公司内网 npm registry 限流了。遇到这种情况先看错误信息是发生在“解析阶段”还是“fetching”阶段。如果是 fetching先检查 registry 配置pnpm config get registry如果指向的是私有 registry看看是否需要更新 token。也可以把超时时间调大network-timeout600000 fetch-retries5 fetch-retry-factor2另外pnpm 下载失败还有一种常见形态明明 store 里已经有这个包安装时却还在重新下载。这通常是因为 lockfile 里的 integrity 值变了或者verify-store-integrity开启了深度校验。旧版本对超大 store 的校验是很耗时的pnpm 12 用 Rust 重新实现了校验逻辑快了很多我才敢把这个配置保持开启。5.2 安装到自定义路径 / D 盘的问题热词里有人问 pnpm 怎么安装到 D 盘。这个得分两种情况。如果是 npm 全局安装的 pnpm 可执行文件想把命令本身放到 D 盘可以用 npm 的 prefix 配置npm config set prefix D:\pnpm npm i -g pnpm12然后把D:\pnpm加到 PATH。如果是想让项目的依赖安装到 D 盘那就是改变 store 目录的问题store-dirD:\.pnpm-storepnpm 所有硬链接都基于 store所以只要把 store 指到 D 盘node_modules 里的硬链接指向的物理文件就在 D 盘对 C 盘空间焦虑很友好。但提醒一句硬链接不能跨卷node_modules 如果也在 C 盘package-import-method必须保持默认的硬链接模式才能在 C 盘 node_modules 和 D 盘 store 之间建立链接。实测这个组合在 pnpm 12 下没问题。5.3 Windows 控制台输出乱码和编码问题升级 pnpm 12 后我在 Windows Terminal 里跑 install日志出现了一些乱码方块。这和 Rust 的终端输出编码有关。Rust 侧日志默认走 UTF-8而 Windows 控制台老代码页可能是 GBK 或正在切换的 UTF-8。解决方法是在执行前加一句chcp 65001或者在 PowerShell 里设置$OutputEncoding [System.Text.Encoding]::UTF8。如果是在 CI 里跑 Windows runner尽量在 pipeline yaml 里把环境变量PYTHONIOENCODING或LANGC.UTF-8配上虽然这不是 Python 项目但对原生模块输出同样有效。这个问题不影响安装结果看着糟心而已。5.4 与 electron / node-gyp 等原生工具链的兼容性用 pnpm 12 配置 electron 打包时最需要注意的不是 pnpm 本身而是 electron 的二进制下载脚本。electron 的 postinstall 脚本习惯用环境变量ELECTRON_MIRROR来指定镜像源这个跟 pnpm 的镜像配置是两套体系。我在项目根目录 .npmrc 里配了 electron 镜像后pnpm install 偶尔会先于 postinstall 阶段把包解压出来导致 electron 脚本找不到源文件目录。后来在 package.json 里给 electron 单独加了installConfig规避。node-gyp 生态也一样pnpm 的符号链接结构会让某些构建脚本找不到头文件。常见解法是给项目开node-linkerhoisted但不建议全仓库开。pnpm 12 对onlyBuiltDependencies字段做了更严格的默认行为如果你升级后发现某个原生模块没有自动执行编译脚本检查一下项目里有没有{ pnpm: { onlyBuiltDependencies: [electron, esbuild, sharp] } }6. 最后说点实在的我个人实际用下来pnpm 12 这次 Rust 内核不是营销概念冷缓存安装和 lockfile 解析的提速体感非常明显。如果你维护的是上百个包、几万依赖的大型仓库升级回报很高如果只是一个几十个依赖的小项目感受可能没那么深刻但也基本不会变差。升级之前记得看两点Node 版本是否满足 18.12以及 CI 镜像里的 Corepack 版本是否足够新。这两点不出问题迁移一般五分钟就能搞定。如果后续要在这个方向继续深挖我会把 Rust worker 的并发模型和文件系统行为拆开再写一篇那些对“为什么快”和“极限在哪里”感兴趣的朋友可以先关注着。