
pnpm 12 正式发布之后很多人第一句话就问 Rust 重写后到底能快多少。先给结论单条 install 的启动阶段和依赖链接阶段确实会比旧版本更利落但“快多少”不能用一个统一倍率概括要看你项目的依赖规模、缓存状态、Node 版本和网络环境。这篇不是替 pnpm 做宣传而是站在从 npm 迁过来、准备升级到 12、以及要在 CI 里跑批量任务的角度把安装、构建、部署、性能对比和问题排查的完整流程拆一遍。适合前端开发、全栈工程师以及负责前端工程化治理的同学。1. 先搞清楚 Rust 重写到底重写了哪一段1.1 为什么包管理器值得用 Rust 重写pnpm 的核心工作不是把压缩包下载下来那么简单。它要做版本解析、依赖关系计算、内容寻址存储、符号链接创建、构建脚本调度还要在 monorepo 里处理工作区依赖。这些步骤在 Node.js 原生栈里写问题不是不能跑而是依赖一多解析和校验带来的开销会变得不可控。Rust 进到 pnpm 的底层链路之后比较明显的变化在三个地方命令行启动速度更快。以前会被 Node 运行时启动拖一部分时间现在二进制启动路径更短。依赖解析和安装调度阶段的逻辑更集中CPU 占用模式更稳定GC 抖动少。并发场景下的数据一致性处理更严格团队维护起来更有底气。注意这不是说 pnpm 12 的所有代码都用 Rust 重写了。更准确的说法是核心性能敏感路径交给了 Rust 组件外围的命令体验还是 Node 生态的插件和脚本体系。理解这一点有实际价值你在处理问题时不需要去读 Rust 源码也不需要为了跑 pnpm 安装 Rust 工具链。1.2 内容寻址 store 和硬链接机制真正让 pnpm 和其他包管理器拉开差距的不只是 Rust而是它一直坚持的内容寻址存储。简单解释一下npm 安装依赖时通常会直接铺开一个扁平的 node_modules同一个版本的包如果被多个项目需要就得在磁盘上各存一份。pnpm 不一样它把所有下载过的包放到一个统一的 store 目录里通过文件名和内容哈希来保证唯一性。项目安装时实际文件并不复制到项目的 node_modules 下而是通过硬链接或符号链接指向 store 里的原文件。这样一来磁盘占用会明显下降安装时间也会减少。Rust 重写之后链接的创建和校验速度更快尤其是依赖数量超过几百个的项目你会更容易感受到这一点。如果你以前用过 npm第一次看到 pnpm 安装后的 node_modules 结构可能会懵。里面是一堆符号链接真正的包文件藏在 .pnpm 目录里。这个结构不是 bug是设计。1.3 用 pnpm 12 不需要学 Rust这里要顺手纠正一个网上常见的误会。搜索“pnpm Rust”的时候经常能看到一堆 Rust 安装教程、Rust 开发环境搭建、Rust 离线安装源码之类的内容。这些和普通前端项目的 pnpm 使用没有直接关系。pnpm 发布的是编译好的可执行文件你是使用者不是开发者。如果你只是想在项目里跑pnpm install或者写 CI 流程完全不需要安装 Cargo也不需要配置 Rust 的 toolchain。只有当你准备给 pnpm 做二次开发、提交插件、研究源码时才需要 Rust 环境和整套编译工具链。普通项目迁移到 pnpm 12只需要关心一件事Node 版本够不够。2. 升级前先确认 Node 版本、镜像和缓存状态2.1 Node 版本过低是最大的坑pnpm 12 对 Node 版本有要求。网上会看到类似这样的报错error: this version of pnpm requires at least node.js v22.13 the current version is ...这个报错的意思很直白当前 Node 版本低于 pnpm 12 要求的最低版本。遇到这种情况第一步不是去降 pnpm而是把 Node 升级到合理版本。我建议升级前先确认一下当前环境node -v npm -v pnpm -v这里有个容易忽略的点用node -v看到的版本不一定是你执行pnpm时使用的那一个。如果你用了 nvm、fnm、Volta 这类版本管理工具要确认当前 shell 是否切到了正确的 Node 版本。Windows 上尤其容易出现多版本并存的情况一个 Node 装在了 Program Files另一个装在了 nvm 的目录PATH 顺序一变pnpm 就会认错运行时。如果项目里还有老代码依赖 Node 16 或 Node 18不一定要立刻升到 22。可以先在一个分支里升级 Node 和 pnpm跑一遍构建和生产用例确认兼容性之后再推广到团队。2.2 四种常见安装方式pnpm 的安装方式不少不同环境的推荐做法不一样我把常见方式列出来安装方式命令适用场景npm 全局安装npm install -g pnpm最通用任何有 Node 的环境都能用Corepack 管理corepack enable后corepack prepare pnpmlatest --activateNode 自带 Corepack 时比较省事独立可执行包npm install -g pnpm/exe避免和 npm 全局路径混在一起包管理器自托管下载官方预编译产物CI 或离线环境升级过程中有一个很常见的报错pnpm : 无法将“pnpm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称或者是pnpm 不是内部或外部命令也不是可运行的程序或批处理文件。这两种报错本质一样系统在 PATH 里找不到 pnpm。先执行npm config get prefix看看 npm 全局安装目录在哪再把这个目录加到系统 PATH。Windows 下改完 PATH 后记得重新打开终端最好是重新开一个新的 PowerShell 窗口不要只刷新会话变量。如果是用 npx 临时跑一次比如npx pnpm -v确实能启动但这不是长期可用的姿势。日常开发要的是全局命令能直接命中建议把环境变量一次配好。2.3 镜像源和下载失败的调整思路安装 pnpm 或者安装依赖时最烦人的是下载超时和包体下载失败。你看到“pnpm 下载失败”未必是工具坏了大概率是连接默认 registry 不稳定或者公司网络对某些域名做了限制。常见做法是设置国内镜像pnpm config set registry https://registry.npmmirror.com也可以用阿里云或腾讯云等团队维护的 npm 镜像不过镜像地址会变建议以你的实际网络验证为准。设置完之后执行pnpm config get registry先确认配置生效再重新pnpm install。除了 registry还有一个容易被忽略的地方pnpm store。如果之前用 npm 下载过一些包pnpm 会把下载内容重新整理到自己的 store 目录。如果你更换了磁盘、换了电脑或者把项目挂到了网络盘上store 路径变化会影响链接结果。可以用pnpm store path查看当前 store 的完整路径。另外新版 pnpm 安装依赖时如果发现某些包需要执行安装脚本可能会提示你运行pnpm approve-builds这个命令的目的是让你选择允许哪些依赖执行 postinstall 脚本。默认阻止所有依赖的构建脚本是为了安全性考虑。到了你确实需要某个依赖跑脚本时再运行这个命令交互式勾选而不是用--ignore-scripts一棍子打死。3. 从 npm 迁移到 pnpmnode_modules 结构为什么会不一样3.1 幽灵依赖问题用 npm 安装依赖时所有的依赖和间接依赖都会被提升到 node_modules 根目录。好处是代码里 import 一个没有直接声明的包时也能跑通。坏处也很明显你根本没在 package.json 里声明它却悄悄用了它一旦升级或删除这个间接依赖项目就会莫名其妙报错。这种依赖在工程上叫“幽灵依赖”。pnpm 默认不提升所有包它把直接的依赖装在最外层间接依赖收进 .pnpm 内部的集中式结构里。所以你安装完成后项目里并没有“所有包都在顶层”这件事。如果从 npm 直接切到 pnpm最可能出现的现象是install 成功但pnpm run build时报错某个模块找不到。最终原因通常是代码里使用了未声明依赖或者某个工具隐式访问了提升目录。3.2 一个迁移步骤的推荐顺序迁移不要只改一个安装命令就完事我一般建议按这个顺序在 git 分支上新建一个迁移分支。备份 package-lock.json 或 yarn.lock。删除 node_modules 和原有锁文件。把 package.json 里的包管理器字段设置为 pnpm。执行pnpm install生成 pnpm-lock.yaml。运行pnpm run build和项目自带的测试用例。处理缺少声明依赖的报错。第 4 步里如果项目支持 Corepack可以在 package.json 里加{ packageManager: pnpm12.x.x }这样团队其他成员执行命令时Corepack 会自动切换到对应版本减少“我这里能跑你那里不行”的版本不一致问题。3.3 处理未声明依赖的写法如果构建时报错“Cannot find module”先别急着把依赖改成--shamefully-hoist或者关闭严格结构。更推荐的做法是找出哪些依赖确实被用到了然后显式补进 package.json。一个快速定位的办法是打开pnpm list查看当前项目的直接依赖和间接依赖层级pnpm list --depth 5如果报错的包是某个直接依赖的间接依赖你在代码里引用了它就应该判断它是否适合直接声明。适合就直接加进 dependencies不适合就改代码不要继续依赖提升行为。只有极少数工具链比如某些老版本 Electron 构建脚本或者依赖深度解析的 monorepo 工具才需要用shamefully-hoist来模拟 npm 的扁平结构。从工程维护角度来说这种方案是最后手段不建议一上来就开。4. 单项目跑通 install再处理 build 和部署4.1 第一次 install 时重点看什么第一次安装依赖先不要急着开最高并发。可以执行pnpm install --reporter append-only这种输出模式更接近日志流。安装完成之后从结果里看几个信息是否报依赖解析错误是否执行了 postinstall 脚本总耗时store 路径变化是否有包被跳过或复用如果你发现安装过程非常久并且卡在某个包下载那问题大概率在网络侧。先看日志里的包名和地址再用 curl 或者 wget 手动拉一下这个地址判断是网络访问不了还是包源响应慢。盲目提高--network-timeout只能缓解暂时超时解决不了源头不稳定。4.2 pnpm run build 的产物怎么交给 Nginx很多前端团队项目中用的是 Vite、Webpack 或 Next.js。pnpm run build之后产物一般会输出到 dist、build、.next 或 out 这类目录。下面是一个比较常见的部署方式以 Nginx 为例server { listen 80; server_name example.com; root /var/www/my-project/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }这里的核心点是try_files $uri $uri/ /index.html。前端路由如果是 History 模式不写这一行的话刷新二级页面很容易 404。有一个经常被忽略的细节用 pnpm 构建后dist 目录里的文件权限可能是当前构建用户生成的。部署时若用 nginx 用户托管静态文件需要确保 nginx 用户能读取这些文件否则页面空白或者 Nginx 返回 403。如果你的项目不是纯静态资源而是 Node 服务端渲染那就不能只开静态服务器。需要先pnpm build再用pnpm start启动应用服务Nginx 只做反向代理。location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }4.3 构建常见报错的排查思路项目构建时报错最典型的不是“pnpm 有问题”而是脚本兼容和路径问题。比如pnpm run build报错“xxx command not found”。这个 xxx 可能是 node、npm、yarn、npx 或项目自定义脚本。pnpm run 执行脚本时会把项目的 node_modules/.bin 放进 PATH但不会自动找到全局工具。解决办法是把这个工具改为项目依赖而不是依赖全局环境。再比如“postinstall script failed”。这类错误要在 install 日志里往前翻找到具体是哪个包、哪条脚本、哪个退出码。不要只看最后一行红色错误。很多情况下是依赖里的二进制下载失败或者是 Python、C 编译链缺失和 pnpm 本身没有关系。如果你需要跳过脚本跑一次纯安装可以临时使用pnpm install --ignore-scripts但要注意这只是排查手段。如果把依赖安装脚本全跳过很多需要编译的原生模块产出的二进制就会缺失项目跑起来会更奇怪。5. 性能对比怎么做才靠谱5.1 冷启动和热启动必须分开测聊“Rust 重写后快多少”最怕拿一个已经缓存完的环境去测第二次安装然后宣布“快了很多倍”。因为第二次安装可能直接在 store 里复用已经下载过的包网络消耗几乎为零这个结果不能代表真实冷启动。要对比必须先分清场景冷安装清空 store 缓存清空 node_modules重新下载所有依赖。热安装保留 store 和锁文件只对增量部分做处理。增量安装只新增了一个小依赖看需要多久完成。不同场景的判断标准完全不同。冷安装主要看网络和解析能力热安装主要看文件链接和校验速度增量安装主要看是否存在冗余遍历。5.2 记录哪些指标才不算白测建议每次对比至少记录这些指标指标含义怎么看总耗时install 或 run 命令的墙钟时间快速感知整体差距解析时间解析依赖树的时间判断版本解析算法优劣下载耗时网络下载内容的时间网络影响大时不能归功于工具链接耗时创建硬链接和符号链接的时间Rust 重写最有价值的区域磁盘占用node_modules 和 store 的体积验证空间节省情况缓存命中率store 内已有文件的比例判断热安装是否有复用我自己做对比时不会只跑一次。一般跑三次取中间值。如果有某次安装因为网络抖动特别慢先把那次剔除再看稳定数据。5.3 用命令记录耗时Linux 和 macOS 下可以这样记录/usr/bin/time -v pnpm install也可以简单点time pnpm installWindows PowerShell 下可以用Measure-Command { pnpm install }如果你想看更细的日志可以开 verbose 模式pnpm install --reporter ndjson不过这种输出刷屏很厉害更适合导入到文件里分析不适合直接读。5.4 安装慢不一定是 pnpm 的锅热搜词里“pnpm install 延长等待时间”这类问题经常被误判为版本问题。我见过一个项目安装耗时三分钟最后排查发现是 registry 配置指向了一个很慢的平台pnpm 本身只占了不到三分之一时长。遇到安装慢按这个顺序排查看是不是首次下载store 里没有缓存。看日志里是卡在 download、resolve 还是 link。手动下载一个最大依赖包的 tarball测试网络速度。临时切换镜像源再测一次。对比不同 pnpm 版本在同样缓存下的差异。不要一上来就把并发调成 64也不要急着换回 npm。先找到瓶颈在哪一段再决定改参数还是改网络配置。6. CI 和 monorepo 环境下不能只关注下载速度6.1 CI 里的缓存策略本地环境可以容忍一个较大的 storeCI 环境不能随便扔文件因为每次都是新的临时目录。为了提升 CI 构建速度需要把 pnpm store 放到一个可恢复的缓存路径里。常见的做法是# .github/workflows/ci.yml 片段示意 steps: - uses: actions/checkoutv4 - uses: pnpm/action-setupv4 with: version: 12 - uses: actions/setup-nodev4 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run build这里有一个关键习惯CI 里尽量使用--frozen-lockfile。它的作用是严格按 pnpm-lock.yaml 安装锁文件有任何变动都直接报错。这样能避免有人本地改了依赖但没有提交 lock 文件导致 CI 和本地安装结果不一致。6.2 并发、超时和失败重试依赖很大的项目在 CI 上安装时容易卡在依赖下载。不要无限提高并发先观察一段时间内的稳定性。常用参数参数作用--network-timeout 600000加长单次网络请求的超时时间--fetch-retries 5下载失败后的重试次数--fetch-retry-factor 2重试延迟的增长系数--child-concurrency同时运行的子进程数量--workspace-concurrencyworkspace 内并行执行的最大数量如果你不知道该怎么设先在本地跑一次默认配置观察日志里有没有超时重试。有的话再按当前网络情况调整。6.3 monorepo 批量任务要用好 --filterpnpm 很适合 monorepo但如果你在 monorepo 里跑pnpm run build要注意它到底在跑哪些子项目。用--filter可以精确控制pnpm run build --filter your-project/core也可以按目录过滤pnpm run build --filter ./packages/*批量发布或批量测试时建议先列出需要执行的包再做全量并行。不然很容易出现依赖关系还没构建完下游包已经启动构建的情况。CI 里看日志也要分清楚pnpm 是把多个包并行构建的一个子项目报错不代表所有包都失败。所以排查时先看具体是哪个 package 卡住再点开对应的日志不要只看主进程退出码。7. 遇到问题先按这个顺序排查7.1 先分类现象项目装了 pnpm 之后报错先不要笼统说“pnpm 有问题”先看现象归属哪一类命令找不到属于 PATH 或安装方式问题。版本报错属于 Node 版本或 pnpm 版本不匹配。依赖下载慢属于网络、镜像、缓存问题。构建脚本失败通常是原生模块编译或未声明依赖问题。安装成功但运行时报模块不存在重点查 node_modules 结构和幽灵依赖。锁文件冲突查 Git 合并冲突和 lock 文件版本。7.2 一个可以直接套用的检查清单检查项使用命令达标标准Node 版本node -v不低于 pnpm 12 要求pnpm 版本pnpm -v确认已切到目标版本命令是否全局可用where pnpm或which pnpm能定位到可执行文件registry 是否正常pnpm config get registry返回可访问的镜像地址store 路径是否稳定pnpm store path确认路径存在且非临时目录lock 文件是否存在查看 pnpm-lock.yaml提交前至少有一份脚本是否跑通pnpm run build在默认配置下能成功7.3 常见误判有一个非常常见的场景Windows 用户用 PowerShell 执行pnpm提示“无法将 pnpm 项识别为 cmdlet”。很多人以为是 pnpm 坏了到处卸载重装。实际上大部分原因就是 npm 全局目录没有进 PATH或者 PATH 环境变量改完之后没有重新打开终端。还有一个场景pnpm 安装成功后执行pnpm run dev报错项目里配置了.npmrc或.pnpmfile.cjs。如果你是从旧的 pnpm 6 或 8 项目升上来的这些配置里的字段可能已经废弃需要逐一确认。另一个误判是“pnpm 安装太快是不是没装全”。如果你用的是热安装大部分包在 store 里有缓存安装过程非常快是正常的。判断到底有没有装全看两个东西一是退出码是否为 0二是pnpm list的输出是否完整不要只看终端刷屏速度。7.4 清理和重装的小技巧遇到实在查不清的依赖问题可以按这个顺序重置一次pnpm store prunerm -rf node_modulesrm -rf pnpm-lock.yamlpnpm install但注意清空 lock 文件再重新生成会产生一次新的依赖解析。如果你对 lock 文件的版本变化很敏感清理之前一定要保留原 lock 文件备份。生产环境或多人协作时不建议随便删除 lock 文件。如果只是想清掉全局 pnpm可以执行npm rm -g pnpm如果是通过 pnpm/exe 安装的npm rm -g pnpm/exe如果再配合 Corepack还要确认 Corepack 里是否还缓存了 pnpm 的版本避免卸载之后pnpm -v仍然能用。7.5 留档和团队规范最后建议团队把 pnpm 使用规范写进项目文档不用写太长至少包含这几条使用统一 Node 版本必要时加 .nvmrc。使用 packageManager 字段固定 pnpm 版本。提交 pnpm-lock.yaml 到 git。CI 里使用--frozen-lockfile。新增代理依赖时先确认是否在 package.json 中显式声明。切换镜像时统一 registry不要求每个开发者各自改配置。我在看这个文章时通常不会只回答“pnpm 12 快不快”而是优先看一个项目能不能稳定迁移、批量构建时会不会隐藏问题。如果你正准备升级到 pnpm 12我的建议很直接先选择一个中小型项目做迁移测试把锁文件、镜像和 CI 缓存都跑顺了再逐步推到大项目。等到单任务链路稳定之后再根据日志和耗时数据决定要不要调整并发和缓存策略。性能提升不是一次性结论是一个可以持续优化的过程。