pnpm 12 升级实测

一、pnpm 12 值得升吗?

pnpm 12 值得升。

这话我说得干脆,因为不是看发布说明得出的。11.2.2 和 12.8.1 装到同一台机器上,同一个项目,两边各跑一轮,写着写着发现发布说明里有两类,一类我一测就对上,一类我怎么测都对不上。这两类得分开讲。

装依赖慢起来,不同的人碰到的地方不一样。CI 上等一次 install,按分钟算;node_modules 把磁盘吃掉一块,删了心疼,留着占地;换包管理器,还得先想想流水线脚本里哪几条命令要重写。pnpm 靠硬链接省磁盘、把依赖结构管得很死,成了很多人的默认选择。v12 做的最大一件事是全量 Rust 重写,官方原话是彻底告别 TS+Node 实现。

升级之前,我心里有三样担心。

兼容性最要命。命令行、配置、lockfile、node_modules 这四样只要有一处对不上,升级就不是十分钟能收场的事,而发布说明给的承诺是几乎零迁移成本。

数字也容易糊弄人。我给自己定了个规矩,宣传材料里的倍数一律跳过,只看带场景的原数。

再就是规模。这些收益落到多大的项目上还有效,发布说明举的例子都是大仓库。

这篇按性能、多语言、依赖管理、安全、工程化五块讲,升级方案和验证放在最后一节。

二、换的是实现,接口没动

先看这张对照表。

维度 v11 v12
实现语言 TypeScript 实现 纯 Rust 原生重写
运行时依赖 依赖 Node22+ 运行 脱离 Node 环境
交付形态 npm 安装的包 独立可执行文件
安全与索引 基础安全能力与 SQLite 索引 供应链防护默认开启与 SQLite 单文件索引

命令行参数、配置文件格式、lockfile 结构、node_modules 布局,这四个是 pnpm 对外的契约。契约不动,内部从 TypeScript 换成 Rust,你的仓库就不用跟着改。要是它顺手把配置格式也重做一遍,今天这篇就得改叫迁移指南了。

「独立可执行文件」这句我动手看了。v12.8.1 的包里躺着一个 59.7MB 的 ELF 文件,里面能找到 rustc 的路径;把它拷到一个只有系统命令的 PATH 下,照样打印版本号。

我这份 v11.2.2 也是单个可执行文件,146MB,里面塞着 Node 运行时,同样不需要外部的 node。所以「脱离 Node」不是 v12 独有的本事,两份单文件发行版都做得到,区别在包里装了什么,以及后面要说的内存账。

四行里,先看运行时依赖。v11 每次调用都先把 Node 拉起来,这笔开销落在每一次 install 上,仓库越大越明显。

那 7 处行为差异会不会砸到我头上?我逐条对过官方公示,多数跟边缘用法有关,正常项目碰不到。公示里写的是命令行、配置文件、lockfile、node_modules 目录结构四项完全兼容,另列出仅 7 处微小行为差异,绝大多数项目无需改代码。

这类差异花半小时自查还是划算的。升之前把 lockfile 复制一份,升完拿它跑一次 frozen-lockfile 安装,看有没有被改写;再跑一遍平时最容易飘的那几条命令,比如带筛选的脚本、带自定义目录布局的构建。差异藏在这些地方,不在主流程里。

你用到了哪些边缘写法,只有你自己清楚。

三、同一台机器上,两个版本差多少

敢直接往项目上套的数字只有一组,所以我自己开了个台子。

环境摆在这儿,能不能照搬你自己判断。机器是 WSL2 里的 Linux x86_64,Node 24.16.0,pnpm 两边分别是 11.2.2 与 12.8.1,registry 走 registry.npmjs.org,基准项目 15 个直接依赖,装完 node_modules 约 43MB。两个版本各用独立的 store,互不污染。

口径这块最容易被含糊过去,我说细一点。三个场景。重复安装,指依赖没变、node_modules 和 store 都在,再跑一次 pnpm install;干净全量安装,指删掉 node_modules、store 留着;首次安装,指两个都清空、老实走网络。重复安装那一组跑 10 次取均值,单次太快,计量工具的精度只有 10ms,测一次不算数;另外两个场景各跑 3 次取中位数。

我测到的结果是这样。

场景 pnpm 11.2.2 pnpm 12.8.1 变化
重复安装(10 次均值) 427ms 21ms 约 20 倍
干净全量安装(3 次中位数) 0.78s 0.08s 约 10 倍
首次安装(3 次中位数) 4.89s 4.03s 约 1.2 倍
重复安装峰值内存 133MB 20MB 约 6.6 倍
干净全量安装峰值内存 491MB 38MB 约 13 倍
首次安装峰值内存 586MB 62MB 约 9.5 倍

内存的变化比时间更显眼,峰值内存最多差到十几倍,方向都是往下走。装完的 node_modules 体积两边都是 43MB,lockfile 测完没被改写。

缓存预热那组,官方说 472ms 变成 15ms,我测到 427ms 变成 21ms,量级和比值几乎一样。干净全量安装那组,他们说 8.2s 到 5s,我这儿对应的是首次安装,4.89s 到 4.03s,方向一致,差在依赖数量和网络速度上。至于 Vercel 那边 21 项目集群提速 64.4%~90.5%,我没有在本机复现这类规模的仓库,只能引用,不能背书。

有一句我没能对上。他们说 v12 用全新 SQLite 单文件索引替代海量 JSON 索引。我翻了两个版本的 store,都是 v11/index.db 这一个 SQLite 文件,json 文件数是零,两边一模一样。这个说法更像是在跟 v9 及更早的版本比,拿来说 v11 到 v12 的变化对不上。你要是冲着这一点升级,可以先放下。

速度从哪来,发布说明给了两条因果,我逐条核了一遍。交付形态换成原生二进制可执行文件,脱离 Node 直接运行,规避了 Node 启动开销;索引换成全新 SQLite 单文件索引,替掉海量 JSON 索引,查询更快。前一条站得住,后一条在你这一代版本里已经发生过了。

内存是另一回事。速度是体验差异,内存是硬失败。官方把依赖解析 OOM 崩溃单列了一条,说明这类崩溃在超大依赖图上真的发生过。v11 的依赖解析在依赖图很大时会把内存吃满,项目大到某个规模就走到那一步。v12 把这一阶段的内存占用压了下来,能扛住更大的依赖图。我这边 491MB 到 38MB 的差距,跟这个方向对得上。

你那边会不会撞上?翻翻 CI 的日志,有没有那种跑一半被 kill 的构建,比看任何倍数都准。

想拿自己的项目核一遍,把下面这段抄走就行。

bash 复制代码
PNPM11=/path/to/pnpm@11/pnpm
PNPM12=/path/to/pnpm@12/pnpm

# 两个版本各跑一遍,这里以 $PNPM12 为例,换成 $PNPM11 再来一次,日志分开记

# 重复安装:依赖和 store 都不动,连跑 10 次取均值
for i in $(seq 1 10); do
  /usr/bin/time -v "$PNPM12" install 2>> warm-12.log
done

# 干净全量安装:删掉 node_modules,store 留着,跑 3 次取中位数
rm -rf node_modules
for i in $(seq 1 3); do
  /usr/bin/time -v "$PNPM12" install 2>> clean-12.log
done

/usr/bin/time -v 连内存峰值一起给,第三节那张表里的内存行就是这么记的。基准项目是 15 个直接依赖,axios、express、typescript 这一批;你不必照抄,换成自己仓库的依赖,在项目根目录跑,口径一致就能比。

四、三种语言进同一个 Workspace,这条比提速值钱

如果这次升级只能留一条,我留多语言。

pnpm 12 把 npm、Python、Cargo 三类依赖放进同一个 Workspace,一条 pnpm install 全装上,不用为了跑一个 Rust 组件再切一次包管理器。前端仓库里塞 Python 脚本、塞 Rust 工具的情况不少见,混着用就得两套工具链,这一条比提速数字更实在。

省下的不只是一条命令。版本以前靠口头约定,或者在 README 里写一句,现在落进同一个 lockfile,跟着仓库走。

Python 补得比我预期的多。以前在 CI 上跑 Python 任务,得先把解释器准备好,现在 pnpm 自动下载适配版本的 Python 解释器,本地不用提前配置环境。另外两处支持是 Python 可编辑包与多平台统一 lockfile。团队里 macOS 和 Linux 上装出来的依赖版本不会分成两套,评审的时候少一类争吵。

Cargo 这边,目标是让前端 Monorepo 能无缝混合 Rust 代码。混进来之后麻烦的是版本,pnpm 12 的做法是统一版本管理与依赖锁定,把两边工具链之间的适配冲突压到配置层解决,而不是留给你在本地各自应付。

配套的还有 pnpm pipeline。我翻了它的 help,它读 pnpm-workspace.yaml 里的 pipelines 段,默认跑名为 default 的那条,做的是模拟 CI 流水线、批量执行 Workspace 任务,可替代部分 Turbo 脚本能力。它替掉的是脚本编排那一段,前提是你的任务能被 Workspace 描述清楚;构建缓存、远端缓存这些 Turbo 擅长的东西,它不接。

你手上要是只有前端仓库,多语言这一段可以先跳过。

五、日常省事的那几处

这几处都不改你的用法,只是每天少几步。

从 --save-types 说起。TS 项目里装业务包,经常还得顺手补一个 @types,漏了就得回头翻代码。这种错难度低、频率高,很难一次改对,只能靠工具堵。v12.6 给的解法是装业务包时自动匹配安装对应的 @types/* 类型包,手动这一步可以省掉。我看了 12.8.1 的 add 帮助,写的是给没有自带类型的包补上 devDependencies 里的 @types。它不是默认行为,得自己加上。

体积和搬运也省了事。pnpm 12 会做全自动依赖去重,重复的依赖收掉,锁文件条目和 node_modules 体积一起降。还有个变化是可迁移 node_modules,打包和部署阶段能直接搬。这条我没有在本机验证过,官方把它算成 v12.6 的一项变化。它的好处在换机器、换流水线的时候,不用重装一遍,CI/CD 里缓存、解压、再校验那几步能省掉一些。

还有 catalog。本地私有包和本地调试包以前管着别扭,版本要么写死要么随缘。现在 catalog 目录支持 file: 与 link: 两种协议,调试期的包和发布后的包共用一份描述,内部包多的工作区感受最明显。

最后一条是后来加的。pnpm add 可以直接吃 Package URL,依赖来源不走常规 registry 的场景多一条路。内部构建产物挂在某个地址上,省掉先发包再装的两步。你手上有没有这种反复补包的动作?

六、默认值前移,我试了它的下限

安全这块的改动一句话能说完,默认值变了。这点我认,也是这次容易被忽略的好事。

yaml 复制代码
minimumReleaseAge: 1d
blockExoticSubdeps: true

两项供应链防护现在是默认开启的。

minimumReleaseAge 默认 1d,意思是新发布的包一天之内不许装。npm 投毒抢的是首发那段时间窗口,作者刚发出来的版本,装的人最少、审查最少,这段时间最容易得手。blockExoticSubdeps 管子依赖,来源不常规的子依赖会被自动拦下来,依赖树深处被塞东西的机会少了。两项都不用你手动配,装完就在。这条从 v11 继承,v12 又强化了一轮。

我试着找了一下它的下限。找一个 3 小时前刚发布的版本装上,v12.8.1 装成功了,没有任何提示;v11.2.2 也装成功,但它在 pnpm-workspace.yaml 里写了一条白名单,并说明这是松散模式放行的结果。再把窗口显式设成 3 天,同一个版本立刻被拦下来,退出码 1。测下来,默认形态是提示加白名单,不是硬拦。

发布说明那句默认禁止,要打个折扣看。

我也保留一点。默认开启意味着你得知道它在那儿。急着用当天刚发布的包,它会拦你一下,你会不会以为是网络问题?这个代价我认,安全默认值比事后救火便宜。要提防的是团队里有人顺手加个忽略开关,那这套默认值就白设了。

同一个文件里我还试了另一件事,写错一个设置名会怎样。v11 静默忽略,什么都不说;v12 会警告,并猜出我想写的可能是哪个键。要是我在 package.json 里用 devEngines 钉住了 12.8.1,它就变成直接失败,退出码 1,并解释项目钉住的版本满足当前运行版本,这些设置不可能是给别的版本用的。以前写错的键不出声,现在会报到脸上,这是升级后容易碰到的一类变化。

锁文件那边也有变化。tar 包哈希校验失败会直接报错,不像旧版静默覆盖 lockfile。这个变化改的是失败发生的时机。旧版静默覆盖,结果是 CI 和本地装出两套依赖,两边对不上,一查就是半天,而且你根本不知道是哪次装的开始不对。新行为把问题挡在安装阶段,lockfile 不会在你不注意的时候被改写,两边装出不同依赖的情况也就少了。

你的项目里,有没有人写过一条没人记得原因的忽略配置?

七、工程化四条里,只有两条我现在用得上

先说 peerDependencies,告警噪音大多从这儿来。版本一不匹配,告警冒一串,动手改又怕影响别人,拖着又被下一个人拖着。噪音的代价是让人对告警脱敏,之后真出问题也不看了。pnpm update --peer 是专门为这件事加的指令,我确认了它的说明,更新对象就是 peerDependencies。以前要么手改,要么写个脚本半处理,有了这条指令,脚本可以退休了。它有个前提,你的项目得真在使用 peer 依赖,纯应用仓库大概感受不到。

再看调度器。pnpm 12 支持自定义任务依赖关系,也支持单任务并发数量控制,资源被抢占的情况能靠配置压下去。仓库里包一多就知道有用,该串的串起来,该并的并起来,机器不会白白被并行压死。

跨平台构建多了个 forceIgnoresPlatform 配置,用来抹平多系统之间的构建差异。Windows、Mac、Linux 三边的人构建结果对不上,是这种仓库里最伤信任的问题。配置能覆盖一部分场景,剩下的还得靠 CI 兜底。

剩下那两条是内部逻辑修复,pnpm deploy 与 --filter 的筛选逻辑重写过,大仓库里按包筛选、分包打包、批量部署,不容易在半路出岔子。日常察觉不到,出问题的时候才知道有没有用。

八、升级一条命令,验证三步

升级这件事,命令只有一条。

bash 复制代码
pnpm self-update

它会把你带到 v12 最新稳定版。要提前做的是把 CI 镜像里的 pnpm 版本一起更新掉,不然流水线上跑的还是老的。他们的结论是现有项目无需改动,配置、代码、lockfile 与 node_modules 都保持原样,那 7 处微小差异在文档里逐条对照就行,业务侧基本察觉不到。

确认没升坏,我按三步走,每步都有判据。判据比步骤重要,升级之后的第一周,人容易把没感觉当成没问题。

第一步看版本,确认自己确实在 v12 上。

bash 复制代码
pnpm --version

第二步测安装。把缓存清掉再装一遍,跟升级前的耗时比。判据是干净安装落在升级前的七成以内,能进这个区间,前面那组数字在你这儿就算成立。

bash 复制代码
pnpm install --frozen-lockfile

第三步跑工程任务,确认筛选、打包、部署这几条流程还是通的。

bash 复制代码
pnpm -r build

三步都过再合主分支。测试多的仓库可以再加一步,把测试跑一遍。lockfile 对不上时安装阶段就会直接报错,不会拖到运行时。我的做法是先拿一个小仓库走完这三步,再动主仓库。

九、你手上的项目属于哪一类

三类的账不一样,分开算。

个人开发者拿到的东西最直接。依赖一变再跑一遍,等待时间接近于零。依赖图大的个人项目不用再撞上依赖解析 OOM 崩溃。类型安装那一步省掉手补 @types 的动作,样板少写几行。

中小型项目更容易算。升级是零成本的,命令行和配置照旧,安全那两项默认值往前挪了一截,兼容风险基本可以排除。团队里没人在意包管理器的时候,这类升级最容易被拖。我的判断是越早越好办,改动量不会随项目变老而变小。

大型 Monorepo 与跨语言项目吃到的收益最多。性能质变、多生态统一管理,还有 CI/CD 上的效率,在这类项目上一起放大。依赖图越大,重复安装省下的时间越多。内存那条也在这类项目上更突出,我测到干净安装的峰值内存差十几倍,仓库再大一圈,这个差距只会更值钱。

结论是这样,所有使用 pnpm 的项目都建议升级,区别只在先后。依赖图大的、跨语言多的排在前面。要不要今天就动,你比我清楚你手上那条流水线有多脆。

十、我的选择和保留

三条主线我先摆出来。Rust 重构带来性能质变,多语言生态打破边界,安全与工程能力各往前走了一步。这三条我都能在上面找到出处。

零迁移配上这些收益,是这次最特别的地方。工具链迭代大多先加能力再补坑,这一次把兼容性放在了前面,我把它算成一次优质迭代。

我的选择是先动一个不关键的小仓库,走完上面那三步,再决定主仓库什么时候跟。要是你手上只有一个仓库,那就直接升,出问题当场能回滚,代价比犹豫低。

保留的地方也写清楚。只有一台机器、一个依赖集、一次测量,官方那组 Vercel 集群的数字我没有复现,发布窗口只探了下限,内存那条只看了峰值。换一台机器、换一套依赖,数字会变,方向大概不会。

pnpm 这套东西的边界也在变。它从前端包管理器起步,现在也管 Python 和 Cargo 的依赖,后面大概还会往别的语言伸,慢慢变成全栈项目工程化管控工具。前端的活它做,前端之外的活也接。

这是我现在的判断,pnpm 再走一两代,上面这些数字大概都要重测。

你手上那份 CI 里,install 那一步占多少时间?

相关推荐
Sirens.2 小时前
Java并发锁详解:六类锁策略与 synchronized 底层原理
java·前端·算法
Ai-_Man2 小时前
您您这可以把Microsofat Copilot的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。AI导出鸭
javascript·人工智能·ai·小程序·电脑·copilot
特创数字科技3 小时前
一个纯本地运行的图片处理工具:压缩 / 裁剪 / 九宫格 / 圆角 / 滤镜 / 拼图 / 水印,终生使用
前端
IT_陈寒4 小时前
Vue的v-if和v-for混用居然是个天坑
前端·人工智能·后端
天衍四九-4 小时前
【无标题】
前端·spring boot·mysql·nginx·docker
广州华水科技4 小时前
2026年单北斗GNSS变形监测系统推荐榜单,解锁GNSS位移监测新高度
前端
xcyxiner4 小时前
DicomViewer24 修复编译bug(window test 失败)
前端·qt
linux_cfan4 小时前
videojs v10 源代码系列解读:34 · `createComposition`:类型安全的冲突检测
前端·安全