pnpm 12.10 悄悄上线了 loaded node linker:node_modules 不再逐文件落地,13000 个文件的项目起进程从 67 毫秒降到 18 毫秒
node_modules 里那几十万个文件,pnpm 12.10 起可以不落地了:兼容的依赖直接由 Node.js 加载器从内容寻址仓库里现取,实测一个存了 13000 个文件的项目里,每个进程的启动开销从 67 毫秒降到 18 毫秒。
逐文件落地的模型,代价比想象中大
pnpm 过去的默认做法是把依赖从全局 store 硬链接进 node_modules/.pnpm/<pkg>@<ver>/node_modules/<pkg>。链接本身很快,但”每个文件都要在项目里出现一次”这件事有三个长期代价:
- 进程启动要为每个包做一次文件系统解析。monorepo 里
pnpm run每调一个脚本就起一个 Node 进程,13000 个存量文件的项目里,这个开销实测是每个进程 67 毫秒,跑 20 个脚本就是一秒多纯损耗。 - 自带 node_modules 的包会把加载路径打断。npm 自己就带着 bundled dependencies,12.10.1 的发布说明写得很直白:这种包只要有一个被装进来,之前会让所有 Node.js 进程都起不来。
- 脚本里调
node会撞上自己的 shim。旧的加载方式下,每个调用node的脚本都会反复重跑自己的 shim,直到以Argument list too long失败为止。
loaded linker:把”落地”和”加载”拆成两件事
pnpm 12.10.0(2026-10-06 发布)引入实验性的 nodeLinker: { type: loaded }:兼容依赖不再复制/链接进项目,而是通过自动注册的 Node.js loader 直接从内容寻址 store 加载,只有不兼容的包才退回落地。配套的 nodeLinker.excluded 负责指定哪些包(以及它们的整棵依赖树)继续安装到全局虚拟仓库里。
配置写在 pnpm-workspace.yaml——pnpm 官方文档明确 nodeLinker、hoistPattern 这类决定 node_modules 结构的设置只能放在这个文件,.npmrc 只管鉴权和 registry:
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
nodeLinker:
type: loaded
# 不兼容的包(自带 node_modules、原生二进制等)照旧落地
excluded:
- 'npm'
- '@swc/core'
before/after 对照:
# before(默认 isolated):装完依赖后,项目里能看到每一个包的文件
node_modules/
.pnpm/
lodash@4.17.21/node_modules/lodash/... # 1000+ 个硬链接
react@19.2.0/node_modules/react/...
# after(type: loaded):兼容的包不再出现在项目里
# 生成物只有三类,全部在 node_modules/.pnpm/ 下
node_modules/
.pnpm/
.store-manifest.json # store 清单
.store-loader.mjs # 自动注册的加载器
.bin/ # bin shim 照常
12.10.1(同日发布)把生成物从项目根收进了 node_modules/.pnpm/——12.10.0 最初会把 .pnpm-store.json、.pnpm-store-loader.mjs 写到项目根,还往根目录和每个 workspace 包写 .pnpm 目录,升级后要删掉这些旧文件再重装。位置收好之后,13000 个文件的项目里,每个进程的启动开销从 67 毫秒降到 18 毫秒;自带 node_modules 的包改为直接从 store 加载,”某个包让所有 Node 进程起不来”和 Argument list too long 这两个报错一起消失。
顺手把 lockfile 的解析设置也记了
同一版本还有个和 CI 强相关的改动:lockfile.includeResolutionSettings: true 让 pnpm-lock.yaml 记录 autoDedupe、dedupeInjectedDeps、dedupePeerDependents、linkWorkspacePackages。之后的安装会把”记录了不同取值的 lockfile”视为过期,记录了 autoDedupe 的 lockfile 在任何机器上都会被复用——pnpm install --frozen-lockfile 之后再跑 pnpm run 不会又拉起一次安装。
这不是理论收益:issue #16583(2026-10-04 提交,pnpm 12.9.0 可复现)里,非 CI 环境下开着 autoDedupe 时,verifyDepsBeforeRun 会在 --frozen-lockfile 之后偷偷起一次非冻结安装,postinstall 被反复触发,正是 lockfile 不记录解析设置导致的。
这个版本还带了几条安全修复:依赖版本的路径穿越不再能写出全局虚拟仓库之外、config dependency 装之前会去 registry 核对完整性、归档元数据超过 64 MiB 直接拒绝读入内存、pnpm audit signatures 改用 lockfile 记录的完整性校验。
三步落地
- 升到 12.10.1 之后先删旧生成物:根目录和各 workspace 包下的
.pnpm-store.json、.pnpm-store-loader.mjs、.pnpm目录清掉再pnpm install,否则新旧两套加载器打架。 - 小范围开实验特性:挑一个进程启动密集的子包(build/lint/test 脚本多的)先加
nodeLinker.type: loaded,跑time pnpm run build对比前后;不兼容的包用nodeLinker.excluded逐个圈出来。 - 把 lockfile 解析设置一并打开:
pnpm-lock.yaml里加上includeResolutionSettings: true,CI 上验证--frozen-lockfile后pnpm run不再触发第二次安装——这条对不依赖实验特性的团队也能立刻生效。
评论区
登录后可评论。