ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

pnpm 12 Rust 内核实测:安装性能提升 30% 的真实验证

pnpm 12 Rust 内核实测:安装性能提升 30% 的真实验证 听说 pnpm 12 把核心逻辑换成了 Rust 实现我第一反应不是刷新闻而是直接把自己手头一个中型项目拉了出来做了一轮完整的构建速度实测。这个项目是团队里的核心管理系统Vue 3 Vite TypeScript三百多个直接依赖lockfile 文件接近两万行不算大但已经属于日常跑构建会明显感觉到等一等的典型体量。实测的目的非常直接Rust 内核到底把哪些环节提速了能快多少以及从 pnpm 11 迁到 pnpm 12 是不是一件平滑的事情。这篇博客就把完整过程记录下来给同样在犹豫要不要升级的同学一个参考。先说结论在同样环境下pnpm 12 的冷安装速度比 pnpm 11 快了大概 30%热安装和增量安装的差距更夸张体感上是喝口水等一等和感觉跑了个寂寞的区别。而且这次回归核心逻辑用 Rust 重写不光是安装变快锁文件解析、依赖版本比对、虚拟 store 目录构建这些平时感知不强但频繁发生的环节也都明显利索了。下面按实测过程一步步展开。1. pnpm 12 换上的 Rust 内核到底动了哪些地方1.1 为什么包管理器也要碰 Rust很多人可能觉得奇怪包管理器不就是把依赖下载下来、再生成一个 node_modules 吗能有多复杂真不是这样。现代包管理器要干的事情包括拉取 registry 元数据、解析整个依赖图谱、做版本冲突消解、计算校验和、管理全局 store、在 node_modules 里创建硬链接、生成和维护 lockfile以及处理各种生命周期脚本。这些环节里有大量 IO 密集和计算密集操作而且依赖越多越复杂性能瓶颈就越明显。JS 本身在这种场景下不算高效。依赖解析和锁文件比对通常涉及大量对象创建、字符串操作和频繁的 GC一旦项目依赖上千纯 JS 实现就会出现肉眼可见的卡顿。Rust 的优势在于没有运行时 GC、内存布局紧凑、对多核并发的支持更直接尤其适合处理遍历依赖树 计算哈希 文件系统操作这类组合任务。pnpm 其实很早就在部分工具链里引入了 Rust 组件但 pnpm 12 这次是把更核心的路径迁到了 Rust 上通过 napi-rs 这类方案编译成 native 模块给上层 JS 调用相当于把最重的活儿交给了原生代码。1.2 这次重构实际加速的四个环节从我实际体感和数据来看pnpm 12 的提速主要集中在四个环节第一是依赖解析resolve。安装依赖时包管理器要挨个问 registry 要每个包的 metadata然后做版本匹配这个步骤在依赖多的时候极其耗时。Rust 版解析器可以更大程度地并发请求和解析减少串行等待。第二是 store 与硬链接管理。pnpm 的全局 store 是它区别于 npm/yarn 的核心设计所有文件都放在 store 里然后通过硬链接放进项目的 node_modules。早期 pnpm 在硬链接创建上用的是 JS 逐个处理几百个依赖时速度还行上千个依赖时就会明显拖沓而 Rust 对底层文件系统调用的掌控能力更强可以更快地批量创建链接。第三是 lockfile 的处理。生成锁文件、比对锁文件、根据锁文件决定哪些包需要重新安装这是安装命令每次运行都必走的一步。旧版 pnpm 在解析一个两万行的 lockfile 时肉眼能感觉到卡一下pnpm 12 基本是秒开。第四是虚拟 store 的目录构建。node_modules/.pnpm 下面会按依赖版本拆出成百上千个目录pnpm 11 构建这套目录结构时项目大了之后每次安装都有明显耗时而这次重写之后这部分也是收益最大的地方之一。2. 实测准备拿什么项目测怎么测才有说服力2.1 测试项目的选择思路测包管理器性能最怕拿一个几十个依赖的 demo 去测那种项目连差距都测不出来。我选了手头两个有代表性的项目做测试。项目 A 是前面说的 Vue 3 企业后台管理系统直接依赖三百多个锁文件接近两万行前端项目的特征是依赖数量多、版本杂、还有不少依赖自带 postinstall 脚本。项目 B 是配套的 NestJS 后端服务直接依赖六七十个但 NestJS 这类的依赖层级特别深间接依赖反而不少。两个项目放一起测基本能覆盖依赖多而杂和依赖层级深两种常见场景。另外我刻意没有选 monorepo 来测。不是说 monorepo 不重要而是 monorepo 本身有很多变量比如 workspace 下多个包的共享依赖、不同包的构建配置这些变量会干扰对包管理器本身性能的判断。先把手头最普通的单仓项目测明白结论更干净。2.2 测试环境与版本基线测试环境是 MacBook Pro M1 Pro、16GB 内存macOS 系统Node 版本统一用 20.11.1。Node 版本必须固定因为不同 Node 版本对包管理器的性能影响还挺明显的。对比的版本我选了四个npm 10.2.4、yarn 1.22.22、pnpm 11.15.2以及这次的主角 pnpm 12.1.0。npm 和 yarn 在这里主要是做一个参照系告诉大家在同一个项目上大家大概处在什么水平重点还是 pnpm 11 和 pnpm 12 的纵向对比。这里要特别说明一个测试细节pnpm 11 和 pnpm 12 读取的 lockfile 结构略有差异我在测 pnpm 12 之前先用它的import机制把旧 lockfile 转换成了新格式保证两边解析的依赖版本一致。npm 和 yarn 不使用 pnpm 的 lockfile而是各自生成自己的锁文件所以它们的数据只做横向参考不参与严格对比。2.3 测试指标与流程设计我设计了五个测试场景每个场景跑三遍取中位数尽量减少偶然波动。第一个是冷安装把 pnpm 的全局 store、项目的 node_modules 全部清空然后执行 install。这个场景模拟的是新机器、CI 从零开始构建或者团队成员第一次拉代码。第二个是热安装保留全局 store但删除项目里的 node_modules。这个场景对应的是换机器或者清除本地 node_modules 后重新安装store 里有缓存不需要重新下载但需要重新解析和构造目录结构。第三个是增量安装保留 store 和 node_modules然后往 package.json 里新增一个依赖再执行 install。这个场景是日常开发中最常发生的操作每加一个包都要跑一次。第四个是锁文件生成分别用 pnpm 11 和 pnpm 12 执行 import 操作把 package-lock.json 转换成 pnpm 的 lockfile计时对比。第五个是带缓存挂载的 CI 模拟尽量模拟云构建场景store 从缓存恢复后执行 install看脚本阶段耗时。所有测试都在同一台机器上进行网络环境一致过程中关闭了 IDE、浏览器等吃资源的程序尽量保证数据可信。3. 构建速度实测数据翻出来说话3.1 冷安装差距最明显的一个场景冷安装的对比数据如下耗时单位是秒场景npm 10yarn 1.22pnpm 11.15pnpm 12.1项目 A 冷安装61.847.334.223.9项目 B 冷安装26.520.115.410.8项目 A 上pnpm 12 比 pnpm 11 快了约 30%比 npm 快了约 61%比 yarn 快了近一半。项目 B 的差距小一些但也保持在 30% 左右的提升幅度。这个结果说明 Rust 内核带来的收益不是偶发性的两个差异明显的项目上都稳定出现。为什么冷安装差距最大因为冷安装时所有环节都要跑一遍下载元数据、解析依赖树、下载包、构建 store、创建硬链接、生成 node_modules。pnpm 11 在这条链路上最慢的几个点比如依赖解析和 store 目录构建恰好都是 Rust 重写收益最大的部分。而 npm 和 yarn 慢得更多的原因在于它们没有 pnpm 那种全局 store 的硬链接机制每个项目都要物理复制一份依赖文件IO 开销大得多。3.2 热安装与增量安装日常开发体感的关键冷安装虽然差距大但毕竟不是天天发生。日常开发里更多的时候是热安装和增量安装。这两个场景的数据如下场景pnpm 11.15pnpm 12.1提升幅度项目 A 热安装6.12.854%项目 B 热安装3.51.654%项目 A 增量安装4.82.254%项目 B 增量安装2.61.254%热安装场景里依赖包本身不需要下载主要耗时在于解析 lockfile、比对当前 node_modules 是否一致、以及重建缺失的目录和链接。pnpm 12 在这个场景直接把一半以上的耗时砍掉了2.8 秒的体感和 6.1 秒完全不是一回事。前者还能接受后者会让人在命令行前不自觉地等一下。增量安装就更有意思了。新增一个依赖时pnpm 11 需要重新解析整个依赖树然后和 lockfile 比对找出哪些地方变了这个过程在依赖多的项目上尤其慢。pnpm 12 用 Rust 重写的解析器在锁定文件解析和差异识别上非常高效2 秒出头的耗时已经让人感知不到卡顿了。3.3 锁文件解析与 CI 场景的表现再补充两个和 CI 关系密切的数据。锁文件转换方面我把项目 A 的 package-lock.json 通过 import 转换成 pnpm 格式。pnpm 11.15 用了 52 秒pnpm 12.1 只用了 18 秒快了一倍多。这个操作虽然不常用但一旦遇到老项目迁移开发者体验差别巨大。CI 模拟场景我用 pnpm 12 挂载了 store 缓存后跑 install耗时 3.1 秒而 pnpm 11 同样条件下是 6.7 秒。有人可能觉得 CI 里反正有缓存install 快慢无所谓但实际上很多 CI 平台对 job 时间有预算限制每次省几秒钟乘以每天十几次构建积少成多也是很大一笔时间成本。整体看下来pnpm 12 的 Rust 重构不是在某个单点上做了优化而是把安装链路上几个重计算环节都提速了所以不管是冷安装还是热安装都能稳定感受到收益。4. 迁移到 pnpm 12 的实操记录4.1 安装与启用过程pnpm 12 的安装方式和平时的版本升级没什么区别。我推荐用 corepack 管理corepack enable corepack prepare pnpm12 --activate也可以用 npm 直接全局安装npm install -g pnpm12装完先确认版本号pnpm --version如果这里直接报错那大概率是 corepack 或环境变量的问题具体排查方法放在后面第五部分讲。升级前我建议先在你本机的另一个目录里建个临时项目跑一遍pnpm install确认源、缓存、全局配置都没问题再切到真实项目操作。别一上来就直接改全局一旦遇到兼容性问题手忙脚乱维修的体验很影响心态。4.2 兼容性观察项目跑起来有没有问题我把两个项目分别切到 pnpm 12 之后install、dev、build 全部跑了一遍没有出现安装失败或者产物异常的问题。前端项目的 Vite 构建时长在升级前后基本持平说明 pnpm 12 的提速集中在依赖管理阶段不会影响项目本身的构建逻辑。但有一个变化值得注意pnpm 12 对一些配置项的校验更严格了。比如.npmrc里如果写了格式不规范的自定义配置以前可能只是忽略或者警告pnpm 12 会直接提示并终止安装。还有 pnpm 10 开始引入的对生命周期脚本的默认限制在 12 里依然生效如果项目里有依赖依赖 postinstall 脚本做编译或者下载操作需要在pnpm.onlyBuiltDependencies里显式声明否则脚本不会执行。我们项目里有一个依赖需要跑 postinstall 脚本升级后第一次 install 就发现脚本没跑翻文档才确认是默认安全策略的问题。这个问题不是 pnpm 12 才有的但如果你是从 pnpm 9 或更早版本直接跳到 12很容易在这里被坑到。4.3 几个需要手动处理的边界情况实际操作中还遇到三个边界情况这里逐一记录。第一个是老项目里的 git 依赖。项目 A 的 package.json 里通过 overrides 指定了一个 git 仓库版本的依赖pnpm 12 首次解析时打了一条警告说这个依赖会以非 lockfile 的方式被处理但不影响安装结果。这类依赖如果后续版本更新了 hashpnpm 12 可能不会自动感知需要自己留意。第二个是自定义 store 路径的问题。如果团队之前配置了store-dir或者virtual-store-dir升级后建议确认一遍路径是否仍然生效。我们 CI 用的就是自定义 store 目录升级后没发现异常但保险起见还是把这个配置在新版本下单独跑了一遍验证。第三个是 lockfile 的提交问题。因为 pnpm 11 和 pnpm 12 对 lockfile 的处理逻辑不同团队里如果有人还在用旧版本切换的人提交了新版 lockfile 之后旧的运行环境可能会触发锁文件重新生成造成无意义的 diff。所以升级要尽量在团队里同步推进别一个人带头切CI 里也要固定 pnpm 版本。5. 常见问题与排查技巧实录5.1 pnpm 命令无法识别的三种典型原因升级和日常使用中最常遇到的坑就是pnpm is not recognized as an internal or external command或者是中文环境的pnpm 不是内部或外部命令。这个问题在 Windows 上尤其多我总结了三个高频原因。第一种是 npm 全局目录没有加到 PATH 环境变量里。用 npm 安装 pnpm 之后可执行文件会放在 npm 的全局 bin 目录如果这个目录不在 PATH 中终端自然找不到命令。解决办法是先查一下 npm 的全局目录npm config get prefix然后把输出目录下的 bin 路径加到系统环境变量里重开终端即可。第二种是 node 版本太旧导致 corepack 不可用。现在 Node 官方版本里自带了 corepack如果 node 版本过老corepack 功能不完整或者没有执行corepack enable之后依然找不到 pnpm。这种情况建议直接升级 Node 到当前 LTS 版本比折腾 corepack 本身省事得多。第三种是 Windows PowerShell 的执行策略限制。PowerShell 默认可能不允许运行脚本表现为执行 pnpm 时直接报错。解决办法是用管理员权限执行Set-ExecutionPolicy RemoteSigned或者直接在项目目录下用 npx 临时调用npx pnpm install先确认 pnpm 本身能跑再排查具体限制来源。5.2 pnpm 下载失败与镜像源问题升级到 pnpm 12 后如果遇到下载失败常见报错包括超时、metadata 获取失败、TLS 证书错误等。这类问题大概率不是 pnpm 本身的 Bug而是网络环境和 registry 源的问题。个人建议是国内用户统一把 registry 切换到 npmmirrorpnpm config set registry https://registry.npmmirror.com如果网络环境不太稳定还可以适当增加重试次数pnpm config set fetch-retries 5 pnpm config set fetch-retry-mintimeout 10000 pnpm config set fetch-retry-maxtimeout 60000顺带提醒一个细节切换 registry 之后新生成的 lockfile 里记录的 tarball 地址会指向新源。如果团队里有人还在用官方源lockfile 就可能在更新、回退、再更新之间反复横跳。所以源的统一和 pnpm 版本一样属于团队级的基本配置最好在文档里固定下来。5.3 lockfile 迁移与旧版本混用pnpm 11 的 lockfile 在 pnpm 12 下打开时会自动触发迁移。第一次执行pnpm install会看到类似Lockfile is up to date, resolution step is skipped或者相反地提示锁文件需要更新的日志总之它不会强制你手动改文件。如果只想生成新 lockfile、不想立刻安装依赖可以执行pnpm install --lockfile-only这样可以把 lockfile 转换过程单独完成方便先在 MR/PR 里查看 diff。但有一点务必注意千万不要在同一个项目里混用 pnpm 11 和 pnpm 12。我试过在本地用 pnpm 12 执行过一次 install然后切回 pnpm 11 再跑它会认为 lockfile 格式有差异进而重新解析并改写文件这会导致锁文件里的内容来回变化甚至影响 CI 构建的稳定性。正确做法是团队约定一个统一版本在 package.json 的 packageManager 字段里声明 pnpm 的版本{ packageManager: pnpm12.1.0 }配合 corepack团队成员只要执行corepack use pnpm12.1.0就能自动切换到指定版本从源头上避免版本不一致。最后分享一个我个人的判断。pnpm 从 11 到 12 并不是那种需要做心理建设的大版本升级依赖安装、日常构建都没有遇到破坏性问题反而在安装速度上给了很明确的回报。如果你手头的项目依赖不算少、团队也已经在用 pnpm我认为找半天时间升级是值得的。唯一要做的就是先统一 pnpm 版本、确认 lockfile 迁移、检查一下生命周期脚本配置。目前我自己的项目已经切到 pnpm 12 跑了两周多暂时没发现任何让人想退回旧版的理由。后面如果拿 monorepo 场景再做一轮测试我还会继续更新这篇的实测数据。
返回列表