
看到 pnpm 12 的发布日志里写着“安装内核切到 Rust 实现”我第一反应不是“哇好快”而是“终于可以拿真实项目跑一次了”。pnpm 本来就是以硬链接和内容寻址存储出名的日常用起来已经比 npm 快不少现在连内核都换掉最让人好奇的当然是构建速度还能不能往上再走一截。这篇文章记录我的一次完整升级与实测从 pnpm 11 升到 pnpm 12在同一个 monorepo 上分别测冷安装、热缓存安装、锁文件解析、执行构建。如果你也维护多包仓库或者负责公司 CI 里的前端任务这份数据应该比“官宣”更接近你能复现的效果。1. 为什么这次“换内核”值得关注1.1 pnpm 到底在优化什么包管理器在前端工程里的角色远比“下载依赖”四个字复杂。npm 3 之后为了缓解嵌套依赖的问题会把依赖拍平到 node_modules 根目录这确实解决了早期“依赖地狱”的问题但也带来了一个很恶心的副作用幽灵依赖。意思是你的代码可以引用一个 package.json 里从来没声明过的包原因是它在 node_modules 根目录里躺着一份。项目小的无所谓项目大了以后依赖关系全靠猜升级一个间接依赖可能导致莫名崩掉。pnpm 走的是另一条路内容寻址存储 硬链接 符号链接。所有包内容先解压进一个全局 store然后项目里的 node_modules 不复制文件而是用硬链接指向 store再用符号链接组织出和依赖关系一致的目录结构。这样同一个包在多个项目里只存一份磁盘占用低安装速度也不依赖网络带宽。pnpm 12 这次把核心安装链路换成 Rust 实现重写的正是这套流程里最吃 CPU 的部分依赖解析、文件下载、校验、解压、链接。所以“换 Rust 内核”不只是性能优化这么简单它是对包管理器最底层的路径做了一次完整的重新实现。对于使用方来说API 还是那个 API但干活的引擎变了。1.2 JavaScript 的瓶颈为什么出现在安装上很多人会问pnpm 原本也用 Node.js 写为什么安装会慢我自己观察下来最大的瓶颈不是网络下载而是依赖图解析和文件系统操作。一个大型 monorepo 的 lockfile 里可能有几千个 resolution 条目每个包要处理版本范围、peerDependencies、optionalDependencies、以及对应的 tarball URL。这些工作大部分是 CPU 密集的解析和内存分配。Node.js 做这种工作并非不行但要压榨到极致Rust 这种没有运行时逻辑负担、能更自由并发的语言显然更有优势。pnpm 12 把“拿到 lockfile 之后到真正往磁盘写文件”这一段改成了 Rust 原生实现等于把最重的活交给了更合适的工具。另外安装过程中有大量文件校验和硬链接操作。校验要用哈希计算链接要挨个处理文件元数据。Rust 可以更高效地并行处理这些 IO 操作而不是在 JS 层来回调度。实测下来多核机器上提升尤其明显。1.3 “换内核”到底换了哪一层我先说明不要以为 pnpm 12 就是“整个软件都用 Rust 重写”。发布日志里说的 Rust 内核更多是指安装引擎也就是负责下载、解压、写 store、生成 node_modules 的核心链路。CLI 交互、插件体系、部分命令可能还是 Node.js 实现这对普通用户反而是好事配置文件、命令结构、workspace 协议基本不用改。升级前我做了一个最小检查清单package.json 里的 packageManager 字段如果锁了旧版本要先更新。CI 里如果直接下载 pnpm 二进制要改成对应 12.x 版本或者用 corepack。项目里如果有自定义的 pnpmfile先确认兼容性否则安装阶段可能直接报错。锁文件版本可能会升级第一次 install 会做一次锁定文件迁移建议本地跑完确认无 diff 再提交。提前处理这些后面实测会顺利很多。2. 升级准备先搞清楚项目在什么环境里跑2.1 我的测试项目长什么样为了不让数据太“玩具”我没有用空项目测试而是拿手头一个真实在跑的 monorepo 做基准。这个仓库包含前端应用、后端服务、共享工具包和组件库一共 24 个 workspace。lockfile 里解析出的依赖总数大概 1900 个node_modules 展开后约 6.2GBpnpm store 在测试前约 8.7GB。机器配置是 Ubuntu 22.04CPU 16 核内存 32GBSSD 是 NVMeNode 版本用的是 20 LTS。为什么强调这些因为安装速度和磁盘、CPU 关系很大尤其是硬链接在同一分区下速度才最快。你用 4 核小机器跑出来的数据可能完全不一样。2.2 升级步骤不重装项目的最小路径升级 pnpm 本身非常简单我用的命令是npm i -g pnpm12 pnpm --version如果你走 corepack也可以corepack prepare pnpm12 --activate这里提醒一句有人会搜索“nvm 安装 pnpm”但 nvm 只负责管理 Node 版本并不能直接安装 pnpm。正确做法是在 nvm 切换到目标 Node 版本后再用 npm 全局安装或者用 corepack 接管。升级到 12 之后我建议先确认 store 路径没变pnpm store path如果路径和之前一致说明 store 可以继续复用不需要重新下载。接着在项目根目录跑一次pnpm install第一次跑会做锁文件格式迁移输出里能看到 lockfile 版本变化。跑完后检查一下 pnpm-lock.yaml 的 diff把正常的变更提交到仓库不要留在本地。2.3 我用的测量方式测构建速度最怕变量太多所以我把测试分成几个固定场景冷安装清空 node_modules同时清空 pnpm store让所有依赖都必须重新下载和写入。热安装store 里已经有全部包但 node_modules 被删掉模拟 CI 缓存命中时的情况。纯解析网络没有波动store 已有全部包用 frozen lockfile 做一次 install主要看 Rust 内核在解析阶段的耗时。构建执行pnpm -r run build看全仓库构建总耗时。为了避免网络抖动影响结论每个场景连续跑 3 次取中位数。冷安装的清理命令是pnpm store prune rm -rf node_modules time pnpm install --frozen-lockfile这里特别说明pnpm store prune会清理全局 store 里未被项目引用的孤儿数据但不会清空所有内容。为了制造真正的冷环境我额外删除了 store 目录这个操作在生产环境不要随便做会拖慢下一次安装。3. 实测结果快的那部分全在安装链路3.1 冷安装最明显的提升冷环境下的数据提升是我这次实测里最夸张的一项。同一个 node_modules 全空、store 全空的项目pnpm 11 冷安装中位数是 86.4 秒pnpm 12 是 52.8 秒整体耗时下降了约 39%。下载量并没有变化依赖包数量也没少为什么能快这么多一方面是因为 Rust 内核在解压和写入 store 阶段并行度更高另一方面是哈希校验、文件去重这些 CPU 密集操作不再受 JS 层的单点调度限制。网络好的时候提升幅度可能没那么吓人但依赖越多这个优势越明显。我还顺便跑了 store 为空的“纯 resolve”场景只跑依赖图解析然后把包写入 store不生成项目里的 node_modules。pnpm 11 大约 11.6 秒pnpm 12 是 3.7 秒。这个场景最能体现 Rust 解析 lockfile 的效率快的理由很简单这类工作不适合一直做字符串处理和对象分配。3.2 硬链接与 node_modules 生成Rust 的甜点区热安装场景能看出 store 复用时 pnpm 12 的表现。我先确保 store 里已经有全部包然后删除项目里的 node_modules跑一次pnpm install --frozen-lockfile。pnpm 11 中位数是 31.2 秒pnpm 12 是 18.9 秒提升约 39%。消耗时间的地方主要是生成硬链接和符号链接。项目里每个依赖都要在 node_modules/.pnpm 目录下建一个目录再把文件从 store 硬链接过去最后按依赖关系创建符号链接。老版本需要 JS 层逐个创建并处理权限Rust 内核可以更简单地批量操作在高并发文件操作上优势非常大。不过这里有个前提store 和项目必须在同一个磁盘分区。硬链接不能跨文件系统如果 store 在一个分区项目在另一个分区pnpm 回退到复制文件速度会明显下降。我见过有人把 store 放到独立数据盘结果安装变慢其实就是这个原因。3.3 全仓库构建提升有限但整体更稳安装阶段的提升是实打实的到了pnpm -r run build这个阶段数据就冷静下来了。测试项目里跑一次全量构建pnpm 11 中位数约 116 秒pnpm 12 约 111 秒差异在 5 秒以内。原因不复杂build 命令真正执行的是 tsc、vite、vitest 这些子进程每个子进程都独立跑在 Node.js 环境里pnpm 内核只负责按拓扑排序启动它们和收集输出。Rust 重写带来的调度效率提升在子进程本身就占大头时很难体现出来。但更让我舒服的是整个并行构建过程中任务的启动顺序和内存占用都更稳定卡在某个包上的概率明显小了。3.4 顺带观察CPU 和内存占用安装过程中我看了一眼系统监控。pnpm 11 安装时 CPU 基本在 1 到 2 个核心附近活动内存峰值大概 420MB。pnpm 12 在同等条件下CPU 能吃满 4 到 6 个核心内存峰值反而降到 310MB 左右。这说明 Rust 内核不只是“更快”它还更擅长利用多核。对本地开发机来说你可能会觉得风扇转得更猛对 CI 来说这意味着同样的安装任务在同样价位的机器上可以更快释放资源。需要提醒的是如果你的 CI 实例 CPU 核心数很少比如只有 1 核 2 核提升幅度会缩水因为并行优势会打折扣。场景pnpm 11 中位数pnpm 12 中位数变化冷安装store 空86.4s52.8s-39%热安装store 有缓存31.2s18.9s-39%纯依赖解析不生成 node_modules11.6s3.7s-68%全仓库run build116s111s-4%4. 常见问题与排查技巧实录4.1 升级后报“pnpm 不是内部或外部命令”这个报错非常经典尤其是 Windows 环境。升级或重装 pnpm 后如果终端提示找不到命令先确认全局 npm 的 bin 目录有没有加入 PATH。可以用npm config get prefix拿到 prefix 后把prefix对应的 bin 目录加进环境变量。如果你用的是 nvm切换 Node 版本后也要注意每个 Node 版本对应的全局包可能是分开的需要重新执行一次全局安装。还有一个更隐蔽的情况旧版 pnpm 是通过独立脚本安装的新版又通过 corepack 或 npm 全局安装两条安装路径的 bin 目录不一样冲突后就会出现命令找不到。解决办法是先把旧版卸载干净再重新安装。4.2 锁文件格式不一致怎么处理升级 pnpm 12 后第一次跑pnpm install常会遇到锁文件 check 失败或者 CI 上报ERR_PNPM_OUTDATED_LOCKFILE。这通常是因为 lockfile 的版本还是旧格式需要在本地用 pnpm 12 重新生成提交。本地执行一遍pnpm install然后检查 pnpm-lock.yaml 第一行的 lockfileVersion 是否变化。如果变了把这个文件提交到 Git。不要在 CI 里用--frozen-lockfile强制安装旧的 lockfile否则会一直报错。比较合理的做法是本地升级和提交锁文件再用 CI 重新安装。4.3 下载失败和 registry 相关的坑“pnpm 下载失败”这个词条经常出现很多情况和包管理器本身无关而是 registry 配置的问题。如果下载 pnpm 自己失败检查 npm 的 registrynpm config get registry如果下载依赖包时部分包 404先看 lockfile 里对应包的 resolved 地址是否可访问再看项目里有没有私有包需要鉴权。pnpm 的 store 是全局的某个包下载失败后下一次安装可能还会命中旧的失败缓存这时候可以针对某个包重新解析pnpm install --force或者先把 store 里对应的缓存清理掉再重新安装。千万不要在生产环境上来就清空整个 store代价很高。4.4 用 pnpm 管理 Node 版本的小技巧升级过程中我还发现 pnpm 本身就是可以管理 Node 版本的很多人搜“pnpm 下载 node 版本”其实可以用pnpm env use --global 20这个命令会帮你在全局安装指定版本的 Node并切换当前环境。对于 CI 和维护多 Node 版本的人来说很方便不用额外引入 nvm。不过在使用前确认一下 pnpm 12 的文档避免和项目里其他 Node 版本管理工具冲突。4.5 卸载 pnpm 时别忘清 store如果你打算删除 pnpm退回 npm 或 yarn千万不要只卸载 pnpm 命令本身。全局 store 还占着磁盘空间不会自动消失。卸载命令可以参考官方文档清理 store 用pnpm store prune注意 prune 只会清掉没有被项目引用的孤儿包如果想让磁盘释放更多需要手动删除整个 store 目录。删除前最好先检查还有哪些项目在共用这个 store否则其他项目下次安装会重新下载。5. 我个人用下来最深的体会一次完整的实测下来我的结论是pnpm 12 的 Rust 内核带来的收益集中在安装阶段尤其是冷安装和 store 缓存复用这两块对日常执行 build 脚本的影响没那么夸张但整个过程的稳定性确实更好。如果你正在维护一个 monorepo或者每次 CI 都要花几十秒安装依赖我建议尽早升级 pnpm 12。收益最大的场景是 CI 冷启动和本地清缓存重装省下来的时间非常可观。升级前记得先走一遍锁文件迁移调整 CI 里的 pnpm 版本和缓存路径剩下的交给内核去跑就行。最后分享一个小技巧升级后可以在项目的 CI 脚本里加一段耗时统计把pnpm install --frozen-lockfile的执行时间输出到日志。我这边连续记录两周后已经能明显看到缓存命中时安装时间稳定在 20 秒以内相比 pnpm 11 时期稳定区间小了很多。这个改动很简单但对后续持续观测构建速度变化很有用。