先总个结:pnpm 的核心优势在于:速度极快、极其节省磁盘空间、能有效避免"幽灵依赖"问题,并且保证了安装的确定性。 下面说说细节。
最最最核心的区别:依赖管理机制
最最最优秀的地方:pnpm省磁盘空间
最最最核心的特点:解决了幽灵依赖的问题
核心架构与依赖管理机制
这是三者最本质的差异,决定了它们在其他方面的表现。
npm 和 Yarn 到目前为止都是 扁平化 node_modules 。它带来一些一些:
-
幽灵依赖: 扁平化后,你可以在代码中直接引用一个并未在你项目 package.json 中声明的包(因为它被你的某个依赖项所依赖,被提升到了顶层 node_modules)。这非常危险,一旦你的某个直接依赖不再依赖这个包,你的代码就会立即报错。
-
非法访问包: 同样,你可以访问到某个依赖包内部的、未导出的子模块,这破坏了封装性。
-
依赖结构不确定性: 同一个 package.json,在不同时间或不同机器上安装,可能会得到不同的 node_modules 结构(依赖提升的行为具有不确定性),这可能导致"在我机器上是好的"这种问题。
然鹅,pnpm是 内容可寻址存储 + 符号链接 它的机制是:
- 全局存储: pnpm 会在你电脑的某个全局目录里,存储所有你曾经安装过的包的硬链接 (Hard Links)。这意味着同一个版本的包(如 lodash@1.0.0)在磁盘上只存在一份。
- 硬链接到虚拟仓库: 当你执行 pnpm install 时,它不会解压包到 node_modules,而是从全局存储创建硬链接到项目下的 .pnpm 虚拟仓库中。这几乎不占用额外空间,速度极快。
- 符号链接到依赖树: 最后,pnpm 会基于你的 package.json 依赖关系,创建一套清晰的符号链接 (Symbolic Links) 到 node_modules 目录。你的项目只能访问到在 package.json 中明确定义的依赖,其他依赖被严格限制在 .pnpm 虚拟仓库内。
优势显而易见:
- 没有幽灵依赖: node_modules 根目录下只有你声明的依赖,结构非常清晰。
- 极致节省空间: 所有项目共享全局存储的同一份包文件,安装 100 个使用 lodash 的项目,磁盘上也只存有一份 lodash 代码。
- 安装速度极快: 大部分情况下,链接文件的速度远快于下载和解压。
性能效率
通常是:pnpm > Yarn > npm
- npm: 较慢。尤其是当网络不稳定或需要安装大量包时(尤其早期还是串行下载)。npm的解析算法差点,所以在第一步就会开始落后。
- Yarn: 比 npm 快。通过并行操作和离线缓存等机制显著提升了速度。
- pnpm: 通常是最快的。得益于其独特的链接机制,在绝大多数场景下(尤其是已有缓存时),它的安装速度远超 npm 和 Yarn。不仅安装快,pnpm add 和 pnpm remove 的速度也更快,因为它处理的文件操作要少得多。
磁盘空间利用率
- npm & Yarn: 每个项目都会将依赖包完整地复制到自己的 node_modules 中。如果有 10 个项目都依赖 lodash@4.17.21,那么磁盘上就会有 10 份一模一样的 lodash 代码。
- pnpm: 极大地节省了磁盘空间。同样 10 个项目依赖 lodash@4.17.21,磁盘上只有一份实体文件,其余 9 个项目都是通过硬链接指向它。这对于拥有大量项目的开发者来说是巨大的福音。
对比表格
特性 | npm | Yarn (v1/Classic) | pnpm |
---|---|---|---|
依赖管理 | 扁平化 node_modules | 扁平化 node_modules | 内容可寻址存储 + 符号链接 |
速度 | 慢 | 快 | 非常快 |
磁盘空间 | 占用多 | 占用多 | 极度节省 |
安全性 | 低(幽灵依赖) | 低(幽灵依赖) | 高(无幽灵依赖) |
node_modules 结构 | 扁平、混乱 | 扁平、混乱 | 严格、清晰 |
锁定文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
CLI 命令 | npm install | yarn add | pnpm add |
建议:任何时候都应该优先考虑使用pnpm
拓展问题
如果我的电脑上安装了nvm等node版本管理器,pnpm在多个node版本中都安装了,那么pnpm还会使用到同一份存在本地磁盘的npm包吗?
答案:是的,即使你使用 nvm 管理多个 Node.js 版本,所有版本的 Node.js 项目共享的仍然是 pnpm 的同一份全局存储(Global Store)。
这正是 pnpm 节省磁盘空间的优势所在,它独立于 Node.js 版本。
pnpm工作原理:
-
全局存储是独立的 :pnpm 的全局存储(通常位于 ~/.pnpm-store 或 ~/Library/pnpm/store on macOS)是一个与 Node.js 版本完全无关的目录。它只是一个按内容寻址的文件仓库,里面存放着所有你曾经安装过的包的实体文件(硬链接的来源)。无论你是用 Node.js 14、16、18 还是 20,lodash@4.17.21 在这个存储库里都只有唯一的一份。
-
项目级别的隔离 :当你切换 Node.js 版本(例如通过 nvm use 16)并进入一个项目运行 pnpm install 时,pnpm 会:
- 从同一个全局存储中创建硬链接,到项目目录下的 .pnpm 文件夹中。
- 根据当前项目的 package.json 和当前环境的 Node.js 版本 和 操作系统(OS) 等信息,生成唯一的 node_modules 结构。
关键点在于:项目中的 node_modules 并不是直接从全局存储读取的,而是通过硬链接"复制"过来的副本。因此,不同 Node.js 版本的项目拥有各自独立的 node_modules 目录,互不干扰。
-
Store 路径的解析 :当你安装 pnpm 时,它的全局存储路径是唯一确定的。无论你通过 nvm 切换到哪个 Node.js 版本,只要你使用的是同一个 pnpm 可执行文件,它都会指向同一个存储路径。
在一个无缓存的情况下,pnpm安装包的速度一定比npm和yarn快吗?
根据上面的介绍,你很容易的想得到,在有安装包的情况下,pnpm由于是建立硬链接指向全局的,所以它当然更快。然鹅!无缓存、首次安装的时候它就不一定了!
install分三步:
- 依赖解析: 计算依赖树,确定需要安装哪些包及其版本。
- 包下载: 从 registry(如 npmjs.com)下载所需的 tarball(.tgz 压缩包)。
- 包写入磁盘: 将下载的压缩包解压,并组织到 node_modules 目录中。
各个步骤阶段对比:
阶段 | npm | Yarn | pnpm | 分析 |
---|---|---|---|---|
依赖解析 | 慢 | 快 | 快 | Yarn 和 pnpm 的解析算法通常更高效。npm 在这方面传统上较慢。 |
包下载 | 慢 | 快 | 快 | 这是无缓存情况下最耗时的阶段! 三者都需要从网络下载完全相同的字节量。Yarn 和 pnpm 都支持并行下载,而 npm 在过去是串行的(新版本也有改进)。因此 Yarn 和 pnpm 在此阶段会非常接近,都可能比 npm 快。 |
写入磁盘 | 慢 (嵌套 -> 扁平化) | 慢 (扁平化) | 略慢 (链接+构建结构) | pnpm 在这个阶段需要做更多计算工作:检查全局存储、创建硬链接、构建严格的 node_modules 布局。而 npm 和 Yarn 则是简单地将包解压到扁平化的目录中。因此,在这个纯写入的阶段,pnpm 理论上可能比 npm/Yarn 稍慢一些,尤其是当项目依赖非常多时,创建大量链接的开销可能会超过解压的开销。 |
总的来说:在无缓存的冷启动场景下,由于网络下载占据了绝大部分时间,而三者下载的包体积基本一致,所以它们的总耗时差距不会像有缓存时那样天差地别。
因此,pnpm和它们在缓存下未必比它们快,but,我们在开发过程中往往避免不了多次安装包,或者你可能不止一个项目。如果都用pnpm的话,后续的install会快很多。