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>。链接本身很快,但”每个文件都要在项目里出现一次”这件事有三个长期代价:

  1. 进程启动要为每个包做一次文件系统解析。monorepo 里 pnpm run 每调一个脚本就起一个 Node 进程,13000 个存量文件的项目里,这个开销实测是每个进程 67 毫秒,跑 20 个脚本就是一秒多纯损耗。
  2. 自带 node_modules 的包会把加载路径打断。npm 自己就带着 bundled dependencies,12.10.1 的发布说明写得很直白:这种包只要有一个被装进来,之前会让所有 Node.js 进程都起不来。
  3. 脚本里调 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 记录的完整性校验。

三步落地

  1. 升到 12.10.1 之后先删旧生成物:根目录和各 workspace 包下的 .pnpm-store.json、.pnpm-store-loader.mjs、.pnpm 目录清掉再 pnpm install,否则新旧两套加载器打架。
  2. 小范围开实验特性:挑一个进程启动密集的子包(build/lint/test 脚本多的)先加 nodeLinker.type: loaded,跑 time pnpm run build 对比前后;不兼容的包用 nodeLinker.excluded 逐个圈出来。
  3. 把 lockfile 解析设置一并打开:pnpm-lock.yaml 里加上 includeResolutionSettings: true,CI 上验证 --frozen-lockfile 后 pnpm run 不再触发第二次安装——这条对不依赖实验特性的团队也能立刻生效。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 198 阅读