
最近把团队的前端工程从 npm 整体切到 pnpm正好赶上 pnpm 12 这一代把核心的依赖解析和安装逻辑用 Rust 重写了一轮。之前看 changelog 的时候还没啥感觉真到自己跑项目才发现原本一个中型 Monorepo 的冷缓存安装要一分钟上下换到 Rust 内核之后直接进了四十秒档。这篇文章就把我这次实测的过程、数据、还有踩过的几个坑原原本本记录下来给还在观望要不要升级 pnpm 12 的朋友做个参考。pnpm 12 换上了 Rust 内核我拿项目实测了一轮构建速度先说结论Rust 内核带来的提速是实打实的尤其在冷缓存安装和锁文件解析阶段最明显但也不是所有场景都无脑快。如果你的项目依赖几百上千个值得升级如果只是个十来个小依赖的玩具项目体感差异不大。为什么会有这种差别下面慢慢拆。1. pnpm 为什么要把核心逻辑换成 Rust 内核1.1 先回顾一下 pnpm 是怎么工作的聊 Rust 内核之前得先把 pnpm 和其他包管理器拉开差距的核心机制说清楚。npm 的 node_modules 是扁平的每个依赖都物理地铺在磁盘上同一个版本的包被多个项目引用就会在各自的 node_modules 里各存一份。yarn 早期也走扁平化路线虽然做了缓存但重复写入磁盘的问题依然存在。pnpm 的解法是“全局内容寻址存储 硬链接/软链接”。所有下载过的包内容统一存放在一个全局 store 里项目里的 node_modules 只是这个 store 的引用。安装时 pnpm 先解析锁文件确定要哪些包然后从 store 复制硬链接到项目的 .pnpm 目录再在对应位置创建软链接。这种方式省磁盘、省网络、安装快但也意味着每次安装都要做大量的文件链接操作和路径计算。依赖一多这些零碎操作的累计耗时就很可观。以前这部分逻辑是用 JavaScript 写的也就是我们常说的 JS 安装器。1.2 Rust 内核到底换了哪一段pnpm 12 这一代核心变化就是把安装流程中计算量最大的“依赖解析、版本匹配、tarball 内容校验、硬链接创建”这些环节从 JavaScript 重写成了 Rust 实现。pnpm 官方把这套东西称为 native 安装器实际发布时编译成了对应的平台二进制。这里要澄清一个容易误解的点不是说整个 pnpm 都用 Rust 写了。命令行入口、插件机制、生命周期脚本调度这些上层逻辑仍然是 Node.js 负责。Rust 主要负责的是“依赖图谱计算 文件系统操作”这种高频、重 IO、重 CPU 的部分也就是安装时最消耗时间的那段路径。那为什么要选 Rust 而不是继续优化 JS关键在两点。一是 Rust 没有 JIT 预热过程冷启动性能好CLI 工具响应更快。二是 Rust 对文件系统批量操作的控制更精细可以做到更少的上下文切换和更高效的内存布局。依赖解析说白了就是大量小文件的统计、哈希、对比、链接这是 Rust 最擅长的领域。1.3 换内核之后性能差在哪我自己实测下来Rust 内核的提速主要来自三个阶段。第一个是锁文件解析阶段。旧版基于 JS 解析 pnpm-lock.yaml依赖一多就需要反复读取、反序列化、做版本区间匹配纯计算型负载。换成 Rust 之后这个阶段耗时直接减半。第二个是tarball 校验与解压阶段。pnpm 会校验下载内容的完整性解压后还要做一次磁盘写入这些操作 Rust 可以多线程并行处理。第三个是硬链接创建阶段。大规模创建硬链接时Rust 的文件操作底层效率高系统调用开销更小。所以如果你项目依赖链很深、包数量很大这几个阶段的累积收益就会非常明显。如果项目只有几个依赖那这点时间差基本可以忽略这也是为什么很多人在小项目上测不出差异。2. 环境准备把 pnpm 12 的 Rust 内核正确打开2.1 版本确认和安装方式要体验 Rust 内核首先得保证版本号到 12.x。这里有个常见的误区很多人之前用 corepack 或 npm 全局装了 pnpm 11后面升级不彻底导致怎么测都感觉没变化。先检查当前版本pnpm --version如果版本不是 12.x需要重新安装。我推荐用 corepack 管理 pnpm因为它可以精确锁定版本以后切换也方便。# 启用 corepackNode.js 16.13 自带 corepack enable # 指定 pnpm 版本 corepack prepare pnpm12.4.2 --activate # 验证 pnpm --version如果你没用 corepack也可以通过 npm 全局安装npm install -g pnpm12这里我更推荐 corepack 方案。因为在团队协作场景下可以在 package.json 里声明 packageManager 字段corepack 会自动拉取对应版本避免出现你本地是 12同事那边还是 10 的尴尬情况。注意如果你之前用 npm 全局安装过 pnpm建议先卸载干净再切 corepack不然容易遇到两个 pnpm 抢 PATH 的幺蛾子。npm rm -g pnpm之后检查which pnpm指向的是不是 corepack 管理的路径。2.2 打开 native 安装器的方法pnpm 12 中Rust 内核的默认开关经历了一个过程。早期版本需要通过配置文件手动开启后续版本逐步扩大了默认启用范围。为了确保拿到完整能力我建议手动显式开启这样能明确知道当前走的是 native 路径。在项目根目录的.npmrc里写入use-node-version20.11.0 enable-pre-post-scriptstrue use-running-store-serverfalse关键的一行是下面这个use-native-installertrue如果你所在团队有统一的.npmrc管理习惯建议提交到仓库里。这样所有人拉下来代码后安装行为都是一致的。2.3 验证当前确实用的是 Rust 内核配置完成后跑一次安装pnpm install安装完成后用下面的命令确认有没有走 native 路径pnpm config get use-native-installer如果是true说明配置生效。另外安装日志里也会出现和以前不同的输出native 安装器处理链接阶段的日志速度明显更快仔细看能感觉到打印频率不是一个量级。如果你发现配置了use-native-installertrue但安装速度没什么变化先检查一下 pnpm 安装器版本是否匹配。pnpm 12 的 native 安装器模块是独立发布的如果你的 pnpm 是通过不完整的镜像源安装的很可能只更新了壳没有把原生二进制拉下来。3. 实测项目的选择与指标设计3.1 测试项目概况我拿来实测的是一个中型的 pnpm workspace 项目一共 18 个 package共享一套依赖。其中核心包涉及 React、TypeScript、Vite、ESLint 等主流工具链锁文件里完整依赖加上传递依赖一共 1200 多个包。这个规模在真实业务项目里很有代表性既不是只有一个 package 的小玩具也不是动辄几千包的巨型 Monorepo。机器配置是 MacBook Pro M1 Pro32GB 内存macOS 14。Node.js 版本是 20.11.0磁盘剩余空间充足。测试前我关闭了所有不必要的后台程序避免系统负载影响结果。3.2 怎么测才公平冷缓存、暖缓存、热缓存经常有人发“包管理器对比测试”的帖子结论却互相矛盾很大原因是测试条件没统一。包管理器安装有一个关键区分缓存里有没有对应的 tarball 和解析结果。我这次分了三种场景冷缓存清空 pnpm store 和项目 node_modules模拟全新环境首次安装。这个测的是完整能力最能体现 Rust 内核的优势。暖缓存保留 pnpm store只删除项目 node_modules。模拟的是换台机器、但全局缓存还在的情况。热缓存store 和 node_modules 都保留只执行pnpm install看增量收敛速度。这个一般在日常开发中用到最多。每种场景至少跑 5 次去掉最高值和最低值取中位数。这样才能排除网络波动和磁盘缓存的偶然影响。3.3 测量工具和命令我用hyperfine来做计时比手动time命令更规范能自动跑多轮并给出统计结果。macOS 上安装很简单brew install hyperfine冷缓存场景先做清理rm -rf node_modules pnpm store prune然后开始计时hyperfine --warmup 1 --runs 5 pnpm install暖缓存场景只删 node_modules不清 storerm -rf node_modules hyperfine --warmup 1 --runs 5 pnpm install热缓存场景直接测hyperfine --warmup 1 --runs 5 pnpm install还顺手测了pnpm install --frozen-lockfile这是 CI 环境最常用的模式不生成锁文件只按锁文件内容安装。这个场景下对解析性能的要求更高Rust 内核的收益也更突出。4. Rust 内核实测数据与瓶颈分析4.1 冷缓存完整安装先看最重量级的冷缓存场景。我用同一份依赖清单、同一台机器分别跑了旧版 JS 安装器通过切换版本实现和 Rust 内核安装器结果如下场景JS 安装器Rust 内核提升幅度冷缓存安装68.4s42.7s37.6% 提升冷缓存 frozen-lockfile59.2s33.1s44.1% 提升暖缓存安装15.6s10.2s34.6% 提升热缓存安装4.8s4.1s14.6% 提升冷缓存安装从 68 秒压到 42 秒左右这个速度我是满意的。frozen-lockfile 模式提升最明显说明锁文件解析和依赖计算确实是瓶颈大头。由于不需要动态解析版本范围Rust 内核直接按已固化的版本做哈希匹配和链接效率非常高。对于 CI 场景来说这 26 秒的节省非常可观。假设一天跑 30 次构建一次省半分钟一天就是 15 分钟一个月下来省出来的时间相当可观。4.2 热缓存安装和锁文件重放热缓存场景的差距没有冷缓存那么大原因在于 store 和 node_modules 都还在pnpm 只需要做一次一致性检查确认现有结构和锁文件没有偏差这个过程本身就不重。但即便如此Rust 内核依然快了一点点说明底层的目录遍历和文件对比操作还是有优化空间。这里有个细节值得注意热缓存场景下耗时的主要瓶颈已经不是 pnpm 本身而是文件系统的 stat 调用和目录遍历。即使换 Rust 内核也绕不开操作系统的文件系统层。所以如果你的热缓存安装还是很慢先怀疑是不是项目里 node_modules 分散文件过多或者存在某些第三方包不打乱结构。4.3 链接阶段耗时为了更细地拆解我单独跑了带--reporterndjson的日志输出分析安装过程中各阶段的耗时。在 Rust 内核版本里依赖解析和硬链接创建耗时大约只占总安装时间的 20%42 秒中的 8 秒左右剩下主要是下载和生命周期脚本执行。JS 安装器在这两个阶段的耗时占比则接近 40%。这解释了一个现象在本地缓存完善、网络不那么快的情况下Rust 内核的收益会更显著因为下载不再是瓶颈CPU 密集的解析和链接操作占比被放大了。4.4 多包仓库下的整体构建我不仅测了安装还顺带测了“安装 构建”的组合链路。方式是把 workspace 里 18 个 package 的buildscript 依次跑完并把pnpm install计时在内。这个更能反映实际开发中 clone 项目后的完整流程。旧版 JS 安装器这条链路总耗时约 127 秒Rust 内核版本约 96 秒。其中构建阶段tsc vite build耗时基本一致差异几乎全部来自安装阶段。这跟我预期一致Rust 内核优化的是依赖管理环节构建本身不受影响。所以不要指望换包管理器能加快编译速度它的价值在于把构建之前那段“准备工作”剪短。5. 收益边界和兼容性雷区5.1 什么时候收益最大从数据和原理两方面看下面这些场景最值得升级到 pnpm 12 Rust 内核依赖数量多500 依赖以上的项目解析和链接的耗时累积明显。Monorepo workspacepackage 数量多共享依赖关系复杂Rust 的多线程处理优势发挥得更充分。CI 流水线每次都是干净环境冷安装frozen-lockfile 模式下收益最大。低频网络环境下载慢的场景Rust 内核省下的解析和链接时间不再被下载时间掩盖。如果你正好在做 monorepo 迁移或者一直觉得 pnpm install 速度不够爽可以拿当前项目先切到 12 试跑一轮大概率会有惊喜。5.2 哪些场景收益不明显坦率说这几种情况换 Rust 内核没什么体感小项目依赖就几十个安装只需两三秒省下的几百毫秒感知不到。项目已经用node-linkerhoisted扁平化模式硬链接和 symlink 逻辑被弱化native 安装器发挥空间变小。网络极慢且没有缓存比如首次从公网拉全部依赖下载时间占大头。另外提醒一句node-linkerhoisted本身就是个“妥协选项”它让 pnpm 的行为更接近 npm但放弃了 pnpm 最大的磁盘优势和依赖隔离优势。如果你为了兼容某些老工具开了这个模式建议仔细想想是不是该换掉工具有限支持而不是牺牲 pnpm 的核心能力。5.3 版本兼容性和坑pnpm 12 对 Node.js 版本有要求官网标注支持 Node 18.12。但如果你的 CI 还在用 Node 16建议先升级 Node 再说。我在测试过程中发现Node 18.12 以下版本在 corepack 激活 pnpm 12 时会有一些奇怪的报错核心原因还是版本太老原生模块 ABI 不匹配。另一个要注意的是与旧锁文件的兼容性。pnpm 12 读取旧版本的pnpm-lock.yaml会自动升级如果你团队里有人还在用 pnpm 10就会出现“我改了锁文件你那边安装报错”的情况。建议统一 lockfile 版本最好把packageManager字段写进 package.json配合 corepack 锁定版本。实战建议升级 pnpm 12 之前先把锁文件提交一次确保升级后只出现预期的 lockfile 变更不要混入其他无关改动。这样万一要回退版本也清楚知道哪些文件需要还原。6. 遇到过的常见问题与排查实录6.1 “Cannot find module /root/.cache/node/corepack/v1/pnpm/12.4.2/bin/pnpm.cjs”这个报错我在升级过程中遇到过一次搜了下社区里也有不少人中招。原因通常有两种corepack 缓存的 pnpm 目录被清理过但入口文件没重建或者 corepack 版本太老对 pnpm 12 的目录结构处理有 bug。解决方法是重新激活或者直接清掉缓存重建corepack disable rm -rf ~/.cache/node/corepack corepack enable corepack prepare pnpm12.4.2 --activate如果是公司统一用 nvm 管理 Node则要特别留意 nvm 切换 node 版本后 corepack 的全局路径可能失效。此时建议在每个项目本地执行corepack prepare来绑定项目级版本而不是依赖全局状态。6.2 下载失败和镜像问题pnpm 12 安装过程中下载依赖如果需要走公网很容易遇到超时或 hash 校验失败。这不是 Rust 内核的问题而是网络环境导致的但发生概率在冷缓存场景下会被放大因为这次是真要拉取所有包。我建议直接在.npmrc里配置国内镜像registryhttps://registry.npmmirror.com如果是公司内网有私服也可以设置成私服地址。设置完之后如果之前已经失败到一半记得先清理失败的缓存pnpm store prune不然有可能出现某些包缓存损坏导致明明配了镜像却依然报校验不一致。6.3 Native 安装器回退到 JS 安装器这是个隐蔽问题。有时候配置了use-native-installertrue但实际跑的时候发现安装日志和以前一模一样速度也没变化。检查后发现pnpm 12 部分版本在遇到某些不支持的锁文件结构时会自动回退到 JS 安装器而且不打明显的警告。排查方法是用 verbose 模式跑一次pnpm install --reporterndjson install.log然后在日志里搜索 native 或 installer看看是否出现 native installer 被禁用或 fallback 的记录。还有一种可能是当前平台没有对应的原生二进制pnpm 官方目前对主流平台支持都挺全但如果你的系统是 ARM 版 Linux 或者某些小众发行版建议先查一下官方支持列表。6.4 与 nvm/corepack 的配合姿势很多人的 pnpm 是通过 nvm 切换 node 后再用 npm i -g pnpm 装的。这种方式在团队协作中最容易出问题。因为不同 node 版本下全局 pnpm 路径不同切换 node 版本后可能直接提示“pnpm 不是内部或外部命令”。我的做法是彻底拥抱 corepackNode 版本统一由 nvm 管理nvm 负责切换 node 本身。每个项目在 package.json 里声明packageManager: pnpm12.4.2。全局不管 pnpm全部通过 corepack 在项目下激活。这样团队里不管谁 clone 代码只要 node 版本满足要求corepack 就会按 packageManager 字段自动准备正确的 pnpm不会出现版本不一致导致的锁文件混乱。最后分享一个我实际操作中的习惯我现在会在 CI 脚本里把pnpm install --frozen-lockfile作为默认安装命令中途多打印一个阶段分隔日志这样一旦安装变慢就能马上定位是下载阶段还是链接阶段出问题。另一点是根据项目规模动态决定是否开启 pnpm 的 side-effects cache开启了能进一步加速构建但副作用是某些不规范的包可能出现行为差异这个需要在自己项目里实测后再决定。如果你刚切到 pnpm 12建议先小范围灰度一个项目跑通后再全量铺开毕竟工具升级最怕的不是性能不达标而是隐藏的兼容性问题。