最近把团队的前端工程从 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,因为它可以精确锁定版本,以后切换也方便。
# 启用 corepack(Node.js 16.13+ 自带) corepack enable # 指定 pnpm 版本 corepack prepare pnpm@12.4.2 --activate # 验证 pnpm --version如果你没用 corepack,也可以通过 npm 全局安装:
npm install -g pnpm@12这里我更推荐 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-version=20.11.0 enable-pre-post-scripts=true use-running-store-server=false关键的一行是下面这个:
use-native-installer=true如果你所在团队有统一的.npmrc管理习惯,建议提交到仓库里。这样所有人拉下来代码后,安装行为都是一致的。
2.3 验证当前确实用的是 Rust 内核
配置完成后,跑一次安装:
pnpm install安装完成后,用下面的命令确认有没有走 native 路径:
pnpm config get use-native-installer如果是true,说明配置生效。另外,安装日志里也会出现和以前不同的输出,native 安装器处理链接阶段的日志速度明显更快,仔细看能感觉到打印频率不是一个量级。
如果你发现配置了use-native-installer=true但安装速度没什么变化,先检查一下 pnpm 安装器版本是否匹配。pnpm 12 的 native 安装器模块是独立发布的,如果你的 pnpm 是通过不完整的镜像源安装的,很可能只更新了壳,没有把原生二进制拉下来。
3. 实测项目的选择与指标设计
3.1 测试项目概况
我拿来实测的是一个中型的 pnpm workspace 项目,一共 18 个 package,共享一套依赖。其中核心包涉及 React、TypeScript、Vite、ESLint 等主流工具链,锁文件里完整依赖加上传递依赖一共 1200 多个包。这个规模在真实业务项目里很有代表性,既不是只有一个 package 的小玩具,也不是动辄几千包的巨型 Monorepo。
机器配置是 MacBook Pro M1 Pro,32GB 内存,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,不清 store:
rm -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.4s | 42.7s | 37.6% 提升 |
| 冷缓存 frozen-lockfile | 59.2s | 33.1s | 44.1% 提升 |
| 暖缓存安装 | 15.6s | 10.2s | 34.6% 提升 |
| 热缓存安装 | 4.8s | 4.1s | 14.6% 提升 |
冷缓存安装从 68 秒压到 42 秒左右,这个速度我是满意的。frozen-lockfile 模式提升最明显,说明锁文件解析和依赖计算确实是瓶颈大头。由于不需要动态解析版本范围,Rust 内核直接按已固化的版本做哈希匹配和链接,效率非常高。
对于 CI 场景来说,这 26 秒的节省非常可观。假设一天跑 30 次构建,一次省半分钟,一天就是 15 分钟,一个月下来省出来的时间相当可观。
4.2 热缓存安装和锁文件重放
热缓存场景的差距没有冷缓存那么大,原因在于 store 和 node_modules 都还在,pnpm 只需要做一次一致性检查,确认现有结构和锁文件没有偏差,这个过程本身就不重。但即便如此,Rust 内核依然快了一点点,说明底层的目录遍历和文件对比操作还是有优化空间。
这里有个细节值得注意:热缓存场景下,耗时的主要瓶颈已经不是 pnpm 本身,而是文件系统的 stat 调用和目录遍历。即使换 Rust 内核,也绕不开操作系统的文件系统层。所以如果你的热缓存安装还是很慢,先怀疑是不是项目里 node_modules 分散文件过多,或者存在某些第三方包不打乱结构。
4.3 链接阶段耗时
为了更细地拆解,我单独跑了带--reporter=ndjson的日志输出,分析安装过程中各阶段的耗时。在 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 workspace:package 数量多,共享依赖关系复杂,Rust 的多线程处理优势发挥得更充分。
- CI 流水线:每次都是干净环境冷安装,frozen-lockfile 模式下收益最大。
- 低频网络环境:下载慢的场景,Rust 内核省下的解析和链接时间不再被下载时间掩盖。
如果你正好在做 monorepo 迁移,或者一直觉得 pnpm install 速度不够爽,可以拿当前项目先切到 12 试跑一轮,大概率会有惊喜。
5.2 哪些场景收益不明显
坦率说,这几种情况换 Rust 内核没什么体感:
- 小项目,依赖就几十个,安装只需两三秒,省下的几百毫秒感知不到。
- 项目已经用
node-linker=hoisted扁平化模式,硬链接和 symlink 逻辑被弱化,native 安装器发挥空间变小。 - 网络极慢且没有缓存,比如首次从公网拉全部依赖,下载时间占大头。
另外提醒一句,node-linker=hoisted本身就是个“妥协选项”,它让 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 pnpm@12.4.2 --activate如果是公司统一用 nvm 管理 Node,则要特别留意 nvm 切换 node 版本后 corepack 的全局路径可能失效。此时建议在每个项目本地执行corepack prepare来绑定项目级版本,而不是依赖全局状态。
6.2 下载失败和镜像问题
pnpm 12 安装过程中,下载依赖如果需要走公网,很容易遇到超时或 hash 校验失败。这不是 Rust 内核的问题,而是网络环境导致的,但发生概率在冷缓存场景下会被放大,因为这次是真要拉取所有包。
我建议直接在.npmrc里配置国内镜像:
registry=https://registry.npmmirror.com如果是公司内网有私服,也可以设置成私服地址。设置完之后,如果之前已经失败到一半,记得先清理失败的缓存:
pnpm store prune不然有可能出现某些包缓存损坏,导致明明配了镜像却依然报校验不一致。
6.3 Native 安装器回退到 JS 安装器
这是个隐蔽问题。有时候配置了use-native-installer=true,但实际跑的时候发现安装日志和以前一模一样,速度也没变化。检查后发现,pnpm 12 部分版本在遇到某些不支持的锁文件结构时,会自动回退到 JS 安装器,而且不打明显的警告。
排查方法是用 verbose 模式跑一次:
pnpm install --reporter=ndjson > 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 不是内部或外部命令”。
我的做法是彻底拥抱 corepack:
- Node 版本统一由 nvm 管理,nvm 负责切换 node 本身。
- 每个项目在 package.json 里声明
"packageManager": "pnpm@12.4.2"。 - 全局不管 pnpm,全部通过 corepack 在项目下激活。
这样团队里不管谁 clone 代码,只要 node 版本满足要求,corepack 就会按 packageManager 字段自动准备正确的 pnpm,不会出现版本不一致导致的锁文件混乱。
最后分享一个我实际操作中的习惯
我现在会在 CI 脚本里把pnpm install --frozen-lockfile作为默认安装命令,中途多打印一个阶段分隔日志,这样一旦安装变慢就能马上定位是下载阶段还是链接阶段出问题。另一点是根据项目规模动态决定是否开启 pnpm 的 side-effects cache,开启了能进一步加速构建,但副作用是某些不规范的包可能出现行为差异,这个需要在自己项目里实测后再决定。如果你刚切到 pnpm 12,建议先小范围灰度一个项目,跑通后再全量铺开,毕竟工具升级最怕的不是性能不达标,而是隐藏的兼容性问题。