Skip to content

Content Addressable Storage of pnpm

pnpm (performant npm) 是一款面向 node.js 的包管理器,其设计目标是 提升安装速度磁盘空间利用效率,相较于 npmyarn classic 等替代方案具有显著优势。pnpm 的核心特性包括:

  • 通过链接(hardlinkreflink)从全局存储库引用文件实现高效率的安装速度。
  • 通过软链(symbolic link)以及结构化目录设计来强制执行严格的依赖访问规则。
  • 内置对单一代码库(monorepo)管理的支持。
  • 通过 pnpm-lock.yaml 文件确保依赖安装的确定性。

pnpm 的基石是 内容寻址存储 机制。与传统包管理器在每个项目中复制依赖文件不同的是 pnpm 会根据文件内容哈希值(content hash)将文件存储在全局(通常是每个磁盘或文件系统一个)的存储库中。这意味着,如果多个不同的包或同一包的不同版本包含了完全相同的文件,那么在物理磁盘上,该文件仅会存储一份。这是 pnpm 实现显著磁盘空间节省的关键所在。

内容寻址存储 (CAS) 架构

内容寻址原理

内容寻址存储核心思想 是:文件以其 内容哈希值 为唯一标识符进行 存储检索。当 pnpm 需要存储一个文件时,他会计算该文件的 内容哈希值。如果全局存储库([[pnpm store path]]/files/)中已存在具有相同哈希值的文件,pnpm 就知道这个文件内容没有变化,无需重复存储。因此,即使 100 个项目都依赖 lodash,或者依赖了 lodash 的不同版本,其中相同的文件在 CAS 中也只存在一份物理副本。这与早期 npmyarnclassic 版本中为每个项目的 node_modules 目录下创建依赖副本的做法形成了鲜明对比。

CAS 的优势

  1. 磁盘空间效率: CAS 显著减少了磁盘空间占用,尤其是在拥有大量项目或依赖项体积庞大的场景下,可以节省数 GB 的空间。更重要的是,当更新依赖时,pnpm 只需向存储库添加版本间 实际发生变化的新文件,而不是复制整个新版本的包。
  2. 安装速度: 由于只需下载存储库中缺失的文件,并主要通过链接(而非复制)文件到项目中,大大减少了 网络传输磁盘 I/O 操作,从而显著提升了安装速度。此外,pnpm 对每个包的安装过程可以 并行处理,进一步提高了效率。

存储位置 (storeDir)

pnpm 存储库的默认位置取决于操作系统和环境变量的设置。可以通过在项目根目录的 .npmrc 文件、工作区配置文件 pnpm-workspace.yaml 或全局配置文件中设置 storeDir 选项来自定义存储库路径。

默认 storeDir 位置

操作系统环境变量条件默认路径
Linux$PNPM_HOME 已设置$PNPM_HOME/store
Linux$XDG_DATA_HOME 已设置$XDG_DATA_HOME/pnpm/store
Linux其他情况~/.local/share/pnpm/store
macOS$PNPM_HOME 已设置$PNPM_HOME/store
macOS其他情况~/Library/pnpm/store
Windows$PNPM_HOME 已设置$PNPM_HOME/store
Windows其他情况~/AppData/Local/pnpm/store

可以通过 pnpm store path 命令查看当前 pnpm 的全局存储库路径。

WARNING

一个至关重要的前提是,为了使 pnpm 能够使用硬链接(hard links)或引用链接(reflinks,也称写时复制链接)来链接文件,存储库必须与进行安装的项目位于同一文件系统或驱动器上。如果 storeDir 指向了不同的驱动器,pnpm 将无法创建这些高效的链接,而会回退到文件复制的方式。这虽然能保证安装成功,但会完全失去 pnpm磁盘空间安装速度 上的核心优势。文件系统的限制是硬链接(通常也包括引用链接)无法跨越分区或驱动器边界。pnpm 会智能地检测这种情况,并在链接不可行时优先保证正确性(通过复制)而非效率。

因此,物理磁盘布局直接影响 pnpm 的性能表现。如果用户没有显式设置 storeDir,并且在不同的驱动器上运行 pnpm installpnpm 会在每个驱动器上自动创建一个独立的存储库(例如,在驱动器 D: 的根目录下创建 D:\\.pnpm-store)。

pnpm-lock.yaml 的作用

pnpm-lock.yaml 文件扮演着至关重要的角色。他精确记录了项目解析后的依赖树结构,包括每个依赖项的确切版本及其自身的依赖关系,从而保证了每次安装都能生成完全相同的 node_modules 结构,实现了确定性安装。此文件还存储了从包注册表(registry)获取的每个依赖包的 integrity 哈希值(通常是 SHA-512)。这个哈希值是该包特定版本内容的唯一 指纹

  • 缓存查找的钥匙: 是 pnpm 在全局内容可寻址存储中查找是否已存在该精确内容包的主要依据。pnpm 会尝试定位到由这个 integrity 标识的缓存文件。

  • 缓存未命中时(已下载过的包)的校验标准: 如果缓存中没有找到对应的包(缓存未命中),pnpm 就需要下载。下载完成后,pnpm-lock.yaml 中的这个 integrity 值就成为必须达到的目标哈希值,pnpm 会对下载的包(tarball 文件)自行计算一遍包的 integrity 值,并将其与 pnpm-lock.yaml 中的 integrity 值进行比较,如果一致,则认为下载的包是完整且未经篡改的。

  • 缓存未命中时(未下载过的包): 如果缓存中没有找到对应的包(缓存未命中),pnpm 就需要下载。下载完成后,pnpm 自行会计算下载的包(tarball 文件)的 integrity 值,并将其与注册表中(默认 http://registry.npmjs.org/)记录的 integrity 值进行比较,如果一致,则认为下载的包是完整且未经篡改的,将其记录到 pnpm-lock.yaml 中。

PS

  1. Tarball 完整性: 在下载过程中或下载完成后,pnpm 会计算下载到的 tarball 文件的哈希值,并将其与 pnpm-lock.yaml 或注册表元数据中记录的预期 integrity 哈希值进行比较。如果不匹配,则表示下载的文件可能已损坏或被篡改,pnpm 会报错并中止安装。值得注意的是,出于安全考虑,npm 生态系统已从早期使用的不安全的 SHA-1 哈希转向了加密强度更高的 SHA-512 哈希。
  2. 存储库完整性 (verifyStoreIntegrity): pnpm 提供了一个配置项 verifyStoreIntegrity,其默认值为 true。当此选项启用时,如果 pnpm 发现 CAS 中某个文件自上次写入后被修改过,他会在将该文件链接到项目 node_modules 之前对其内容进行校验。这为防止存储库意外损坏提供了一层额外的保障,但也会带来微小的性能开销。
  3. 锁文件完整性 (潜在问题): 尽管 pnpm-lock.yaml 保证了安装的确定性,但它本身也可能在版本控制操作(如 git 合并冲突)中被意外破坏或手动错误编辑。目前,如果锁文件与 package.json 的依赖声明一致,pnpm 会倾向于信任锁文件的内容以获得更快的安装速度。然而,社区 讨论 中已经提出了增加锁文件校验和(如 lockfileChecksum 字段)的可能性,以便在不进行完整依赖重新解析的情况下,快速检测锁文件是否被篡改。

这种设计体现了在 安装速度健壮性 之间的权衡。pnpm 默认优先考虑速度,通过信任最新的锁文件来跳过耗时的 依赖解析验证 步骤。然而,锁文件合并冲突或手动编辑可能导致静默的错误。verifyStoreIntegrity 设置提议中的锁文件校验和 机制,都是为了在不过度牺牲性能的前提下增加安全网,这反映了优化包管理器时固有的设计挑战。

文件哈希、索引与 CAS 内存储

单个文件的内容哈希

当一个包的 tarball 被成功下载并校验通过后,pnpm 会对其内容进行处理。一个关键步骤是,pnpm 会为包内的 每一个单独文件 计算其内容哈希值(例如 SHA-512)。

这里需要清晰地区分两种哈希值:包完整性哈希 (package integrity hash)文件内容哈希 (file content hash)

  • 包完整性哈希(记录在 pnpm-lock.yaml 中)用于唯一标识和验证整个包的特定版本(即 tarball 文件本身)。

    下述是 compressible 包的 pnpm-lock.yaml 文件中的相关内容:

    pnpm-lock.yaml
    yaml
    lockfileVersion: '9.0'
    
    settings:
      autoInstallPeers: true
      excludeLinksFromLockfile: false
    
    importers:
      .:
        dependencies:
          compressible:
            specifier: 2.0.18
            version: 2.0.18
    
    packages:
      compressible@2.0.18:
        resolution:
          {
            integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
          }
        engines: { node: '>= 0.6' }
    
      mime-db@1.54.0:
        resolution:
          {
            integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
          }
        engines: { node: '>= 0.6' }
    
    snapshots:
      compressible@2.0.18:
        dependencies:
          mime-db: 1.54.0
    
      mime-db@1.54.0: {}
  • 文件内容哈希是用于标识包内每个独立文件的具体内容,是实现 CAS 存储和文件去重的基础,pnpm 会存储包中的每一个文件并按照文件内容哈希值进行存储(storeDir/v10/files/)。

    下述是 compressible 包在 pnpm 全局存储的索引文件,里面会包含包中所有的文件的 integrity 值,这些都是 pnpm 在下载包时计算得出的。

    compressible@2.0.18.json
    json
    {
      "name": "compressible",
      "version": "2.0.18",
      "requiresBuild": false,
      "files": {
        "LICENSE": {
          "checkedAt": 1745925782020,
          "integrity": "sha512-iwWNtxw0wslfJ9F2n8JXm5zpCbwR7Gu+rFrXsuO44MbiUu1TZov30H2eWdbt7+mvwcEp0nSR1CyUCYsPUbI+WQ==",
          "mode": 420,
          "size": 1233
        },
        "index.js": {
          "checkedAt": 1745925782021,
          "integrity": "sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg==",
          "mode": 420,
          "size": 1038
        },
        "package.json": {
          "checkedAt": 1745925782021,
          "integrity": "sha512-Wk1cB0QJbo7jJUs0U25uFc/2lqJNNbLaXrcPDQ+S9gxPpNcOZYDnHqLtJ0Ep2893chz2MdxN1mArJloAXbTERw==",
          "mode": 420,
          "size": 1311
        },
        "HISTORY.md": {
          "checkedAt": 1745925782022,
          "integrity": "sha512-HjgVdL/NlSwDhkut3BCwnal4ahlNd2eLzPCGPARs0Wy+rjHtAJNL7/s426/dIwbg7NI5wnkxuhLImjx+ULgavg==",
          "mode": 420,
          "size": 1976
        },
        "README.md": {
          "checkedAt": 1745925782022,
          "integrity": "sha512-GahpusTGRt2M/0s2vsEMBfDm0bBXMVlnhBVY+xoDcfBKn4rGbeW9PGQWZ7LM1adQWm8Q3WizIF1D1jwI97IS7w==",
          "mode": 420,
          "size": 1797
        }
      }
    }

CAS 中的包文件存储 (v10/files/)

pnpm 存储库内部有一个特定的目录结构来存放这些基于 内容哈希 的包文件集合。在 pnpmv10 版本中,这个结构位于 storeDir/v10/files/ 路径下。

compressible@2.0.18 为例,该包需要存储的文件集合包含如下:

bash
compressible@2.0.18
├── HISTORY.md
├── index.js
├── LICENSE
├── package.json
└── README.md

以其中的 index.js 为例,检索 compressible@2.0.18 存储在 pnpm 全局存储库中的索引文件路径为:

bash
00/5debecfe5d5b12fc331c884d132539140d68e036224005693af893b054ba68-compressible@2.0.18.json

打开该索引文件,可以看到:

json
{
  "name": "compressible",
  "version": "2.0.18",
  "requiresBuild": false,
  "files": {
    "LICENSE": {
      "checkedAt": 1745925782020,
      "integrity": "sha512-iwWNtxw0wslfJ9F2n8JXm5zpCbwR7Gu+rFrXsuO44MbiUu1TZov30H2eWdbt7+mvwcEp0nSR1CyUCYsPUbI+WQ==",
      "mode": 420,
      "size": 1233
    },
    "index.js": {
      "checkedAt": 1745925782021,
      "integrity": "sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg==",
      "mode": 420,
      "size": 1038
    },
    "package.json": {
      "checkedAt": 1745925782021,
      "integrity": "sha512-Wk1cB0QJbo7jJUs0U25uFc/2lqJNNbLaXrcPDQ+S9gxPpNcOZYDnHqLtJ0Ep2893chz2MdxN1mArJloAXbTERw==",
      "mode": 420,
      "size": 1311
    },
    "HISTORY.md": {
      "checkedAt": 1745925782022,
      "integrity": "sha512-HjgVdL/NlSwDhkut3BCwnal4ahlNd2eLzPCGPARs0Wy+rjHtAJNL7/s426/dIwbg7NI5wnkxuhLImjx+ULgavg==",
      "mode": 420,
      "size": 1976
    },
    "README.md": {
      "checkedAt": 1745925782022,
      "integrity": "sha512-GahpusTGRt2M/0s2vsEMBfDm0bBXMVlnhBVY+xoDcfBKn4rGbeW9PGQWZ7LM1adQWm8Q3WizIF1D1jwI97IS7w==",
      "mode": 420,
      "size": 1797
    }
  }
}

index.js 文件的 integrity 哈希值为

bash
sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg==

pnpm 会根据该 integrity 值按如下方式存储文件:

  1. 内容哈希值通常被转换成十六进制字符串。

    ts
    function base64ToHexNode(base64: string): string {
      return Buffer.from(base64, 'base64').toString('hex');
    }
    const hex = base64ToHexNode(
      'sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg=='.slice(
        7
      )
    );
    console.assert(
      hex ===
        'd7f33f96ca04f3c096a02da2d01f07ff4ff53aa70daaeb4f2a4399eabe154455cb0fc5e47e08818d64dac92aba0f138f1b7b5ae718957dd3d99a7dbfdb05e276'
    );
  2. 获取到的十六进制哈希值的前两个字符被用作一个子目录的名称。

    上述获取的 hex 值为

    bash
    d7f33f96ca04f3c096a02da2d01f07ff4ff53aa70daaeb4f2a4399eabe154455cb0fc5e47e08818d64dac92aba0f138f1b7b5ae718957dd3d99a7dbfdb05e276

    前两个字符为 d7,所以该文件将被存储在 storeDir/v10/files/d7/ 目录下。

  3. 哈希值剩余的部分构成了该文件在子目录中的文件名。

    例子中的剩余部分为

    bash
    f33f96ca04f3c096a02da2d01f07ff4ff53aa70daaeb4f2a4399eabe154455cb0fc5e47e08818d64dac92aba0f138f1b7b5ae718957dd3d99a7dbfdb05e276

通过观察已存储的其他文件路径信息:

bash
00/0a57539c3ea49736380ab214874f4528b455742d1f21c9d8027015cfae1372aa80382fefb88fd0abdd89c27c2d1332561e5186e4fa9ec8c348fc5768ca5b22
00/0cdacf5df3cc29401b02c11f6b4c473b193dfff269021e66d0749bdbe81b1c2336a5a72d1384a0d721b72064857a031a4c937653ed07c5e38731682c4c3eb8
1f/1a6454d1d08337c03084eb136fcd6f1ca1448a7f10cf150bd6dbb65aef9fa3be5b182f02d9127dc0ceadb3ad9a3a0911a98a7b63538afed05b8c5581e357db

可知存储文件的文件名是截取 hash 值的 124 个字符。

综上 compressible@2.0.18/index.js 文件将被存储在

bash
storeDir/v10/files/d7/f33f96ca04f3c096a02da2d01f07ff4ff53aa70daaeb4f2a4399eabe154455cb0fc5e47e08818d64dac92aba0f138f1b7b5ae718957dd3d99a7dbfdb05e276

路径中的 v10 表明这是 pnpm 存储布局的第十个主要版本。这意味着 pnpm 的内部结构可能会随着版本迭代而优化和演进,尽管 CAS 的核心概念保持不变。了解这一点很重要,特别是对于那些试图直接与存储库交互(通常不推荐)的用户来说,因为具体的内部布局是实现细节,可能会发生变化。

CAS 中的包元信息文件存储 (v10/index/)

除了存储单个文件外,pnpm 还需要存储包的元信息,元信息中包含了 包的名称版本文件名文件内容哈希值 的映射关系。存储方式(CAS)与 v10/files/ 类似,不过存储元信息文件的目录与存储包文件的目录是分开的,即 v10/index/

compressible@2.0.18 的包元信息文件内容为例:

compressible@2.0.18.json
jsonc
{
  "name": "compressible",
  "version": "2.0.18",
  "requiresBuild": false,
  "files": {
    "LICENSE": {
      "checkedAt": 1745925782020,
      "integrity": "sha512-iwWNtxw0wslfJ9F2n8JXm5zpCbwR7Gu+rFrXsuO44MbiUu1TZov30H2eWdbt7+mvwcEp0nSR1CyUCYsPUbI+WQ==",
      "mode": 420,
      "size": 1233
    },
    "index.js": {
      "checkedAt": 1745925782021,
      "integrity": "sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg==",
      "mode": 420,
      "size": 1038
    },
    "package.json": {
      "checkedAt": 1745925782021,
      "integrity": "sha512-Wk1cB0QJbo7jJUs0U25uFc/2lqJNNbLaXrcPDQ+S9gxPpNcOZYDnHqLtJ0Ep2893chz2MdxN1mArJloAXbTERw==",
      "mode": 420,
      "size": 1311
    },
    "HISTORY.md": {
      "checkedAt": 1745925782022,
      "integrity": "sha512-HjgVdL/NlSwDhkut3BCwnal4ahlNd2eLzPCGPARs0Wy+rjHtAJNL7/s426/dIwbg7NI5wnkxuhLImjx+ULgavg==",
      "mode": 420,
      "size": 1976
    },
    "README.md": {
      "checkedAt": 1745925782022,
      "integrity": "sha512-GahpusTGRt2M/0s2vsEMBfDm0bBXMVlnhBVY+xoDcfBKn4rGbeW9PGQWZ7LM1adQWm8Q3WizIF1D1jwI97IS7w==",
      "mode": 420,
      "size": 1797
    }
  }
}
  • 内容: 每个元信息文件相当于该包特定版本的一个清单(manifest),他包含了包的元数据(如名称 name 和版本 version),以及最关键的包内所有 原始文件名 与其对应 内容哈希值 的映射关系。通过这个映射,pnpm 知道如何从 v10/files/ 目录中找到构成该包的所有文件,并以 正确的目录结构 将他们 组织 在一起。

  • 命名/位置:

    包元信息文件的命名规则为 [[integrity_hash]]-[[package]]@[[version]].json

    compressible@2.0.18.json 为例,从 pnpm-lock.yaml 中获取的 compressible 包的完整性哈希为:

    bash
    sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==

    通过 base64ToHexNode 函数将该完整性哈希转换为十六进制字符串。

    ts
    const hex = base64ToHexNode(
      'sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=='.slice(
        7
      )
    );
    console.assert(
      hex ===
        '005debecfe5d5b12fc331c884d132539140d68e036224005693af893b054ba68cfb51a460d36699743dbd5708ee89783081769d76e8282cf6c331a928e063246'
    );

    同理使用前两个字符 00 作为子目录名,但通过观察已存储的其他包元信息的文件名称:

    bash
    4809a191f68281dd19697243570e3258268c62c2aa4f3c20571b0289715aa9-@pnpm+patch-package@0.0.1.json
    aa5a6251e7f2de1255b3870b2f9be7e28a82f478bebb03f2f6efadb890269b-base64-js@1.5.1.json
    5d3279e22b928e9df782a9528d4cfb45f5b01d788a4a1aaa5206b931f57d6e-meow@11.0.0.json

    可知元信息文件名是截取 hash 值的 62 个字符。因此 compressible@2.0.18.json 最后的文件路径为:

    bash
    v10/index/00/5debecfe5d5b12fc331c884d132539140d68e036224005693af893b054ba68-compressible@2.0.18.json

CAS 的链接策略

上述已经介绍了 CAS 的存储布局,接下来就要介绍 pnpm 是如何将 CAS 中的文件链接到项目中。

虚拟存储 (node_modules/.pnpm)

pnpm 并不会直接将全局 CAS 中的文件链接到项目根目录下的 node_modules 文件夹。他采用了一种 中间层 结构,即在每个项目的 node_modules 目录下创建一个名为 .pnpm 的隐藏目录,这个目录被称为 虚拟存储

这个 .pnpm 目录才是实际包含指向全局 CAS 中文件链接(hardlinkreflink)的地方。这些链接按照包名和版本的结构进行组织,例如 .pnpm/<pkg-name>@<version>/node_modules/<pkg-name>/

包导入方法 (packageImportMethod)

pnpm 如何将文件从全局 CAS 导入到项目的虚拟存储 (.pnpm) 中,是由 packageImportMethod 配置项控制的。以下是各选项的详细说明:

  • auto (默认值): 这是 pnpm 的首选策略。它会优先尝试使用 clone(即 引用链接 / 写时复制)。如果文件系统不支持 clone,则尝试使用 hardlink(硬链接)。如果硬链接也失败(例如,尝试跨文件系统链接),最后会回退到 copy(普通文件复制)。默认设置旨在智能地选择当前环境下最优的可行选项。
  • clone (引用链接/写时复制): 这是 最快最安全 的方法。他创建一个指向原始文件数据的引用。如果之后修改了项目 node_modules 中的这个文件,文件系统会自动创建一个新的副本,而不会影响 CAS 中的原始文件。这种方式既节省空间(初始时不复制数据),又保证了 隔离性。但它需要底层文件系统的支持(例如 Btrfs, APFS, 以及支持 reflinkXFS)。在速度、空间和安全性(隔离性)之间提供了最佳平衡,但依赖于现代文件系统。
  • hardlink (硬链接): 创建一个硬链接。这意味着项目 node_modules 中的文件条目和 CAS 中的文件条目指向磁盘上完全相同的物理数据块。这种方式在 空间效率上 非常高,因为不占用太多的额外空间(记录 inode 信息)。但是,它的一个重要后果是,如果在项目 node_modules 中直接修改了这个硬链接文件,也会同时修改 CAS 中的原始文件,会无意中破坏 CAS,从而影响其他项目。硬链接要求源文件和链接目标必须在同一文件系统上。极度节省空间,但将项目与存储库紧密耦合,存在意外修改存储库文件的风险。
  • copy (复制): 执行标准的文件复制操作。这是磁盘空间和安装速度效率最低的方式,但它具有普遍适用性,即使跨文件系统也能工作。是万能的后备方案,但牺牲了 pnpm 的主要优势(节省磁盘空间安装速度)。
  • clone-or-copy: 优先尝试 clone,如果不支持则回退到 copy

文件系统的类型(如 Btrfs, XFS, EXT4, APFS)直接决定了 clonehardlink 是否可用。此外,在 docker 构建环境中,由于构建时无法在宿主机文件系统和容器文件系统之间创建 硬链接引用链接,需要采用变通方法,例如使用 BuildKit 的缓存挂载(cache mount)或 pnpm fetch 命令来预先下载依赖。

packageImportMethod 选项比较

选项机制磁盘效率速度文件系统依赖node_modules 可修改性
auto (默认)优先 clone, 再 hardlink, 最后 copy最优可行最优可行取决于实际使用的方法
clone引用链接 (COW)最快是 (Btrfs, APFS, etc.)安全 (修改时创建副本)
hardlink硬链接最高是 (同一文件系统)危险 (直接修改存储库)
copy文件复制安全 (修改的是副本)
clone-or-copy优先 clone, 再 copy依赖 clone 支持依赖 clone 支持是 (for clone)取决于实际使用的方法

node_modules 中的符号链接

链接过程的最后一步是在项目根目录的 node_modules 文件夹中创建符号链接(symlinks)。这些符号链接指向 .pnpm 虚拟存储内相应的包目录。

一个关键的特点是,只有项目的 直接依赖(即在项目 package.json 文件的 dependencies, devDependencies, optionalDependencies 中列出的包)才会被 符号链接node_modules 的根目录。这强制执行了 pnpm 的严格性原则:应用上下文中无法直接 requireimport 未在其 package.json 中声明的传递依赖项(即依赖的依赖)。当 node.js 解析模块时,它会沿着这些 符号链接 找到 .pnpm 目录中实际的包代码。

结论

pnpm 的高效性源于其精心设计的 内容寻址存储 以及 软硬链接 机制。他结合了包完整性校验、对包内每个文件进行内容哈希计算、一个全局内容寻址存储库(CAS)、用于记录包结构的索引文件,以及一套复杂的链接策略(优先使用 引用链接硬链接 将包中所有的文件导入到项目内的虚拟存储 .pnpm,再通过 符号链接 将直接依赖项暴露在根 node_modules 中)。这一系列机制共同作用,实现了显著的 磁盘空间节省安装速度提升

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (41765f2)