iTranslated by AI
Features of pnpm
pnpm is a JavaScript package manager alongside npm and yarn. The name pnpm originates from "performant npm".
It is compatible with npm and has the following features:
- Up to 2x faster compared to other tools
- Efficient disk space usage
- Strict package management, ensuring you can only access packages listed in
package.json
Getting Started with pnpm
pnpm can be installed using the following commands:
# macOS or Linux
$ curl -fsSL https://get.pnpm.io/install.sh | sh -
# Windows (PowerShell)
$ iwr https://get.pnpm.io/install.ps1 -useb | iex
Alternatively, you can install it using npm.
$ npm install -g pnpm
Once the pnpm installation is complete, let's try installing express as a test. To install a package, execute the pnpm install <package-name> command.
$ pnpm install express
Packages: +57
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: ~/Library/pnpm/store/v3
Virtual store is at: node_modules/.pnpm
Progress: resolved 61, reused 0, downloaded 57, added 57, done
dependencies:
+ express 4.18.1
Disk Space Efficient node_modules
As indicated by the message "Packages are hard linked from the content-addressable store to the virtual store.", all actual package files are placed in a globally managed Content-addressable store. On macOS, ~/Library/pnpm/store/v3 is the default storage location.
Every file contained in every package within node_modules is a hard link to the content store.
Once hard links are created in node_modules for all packages, symbolic links are created to reflect the nested dependency graph structure.
In fact, the node_modules directory looks like this:
node_modules/
└── .pnpm/
└── body-parser@1.20.0
└── node_modules
├── body-parser
│ ├── index.js
│ └── package.json
├── bytes -> ../../bytes@3.1.2/node_modules/bytes
├── content-type -> ../../content-type@1.0.4/node_modules/content-
└── unpipe -> ../../unpipe@1.0.0/node_modules/unpipe
└── express@4.18.1
└── node_modules
├── body-parser -> ../../body-parser@1.20.0/node_modules/body-parser
└── express/
├── express -> .pnpm/express@4.18.1/node_modules/express
└── .modules.yaml
In this way, unlike npm or yarn, all packages are stored in a content-addressable store, allowing packages of the same version to be shared across different projects. This achieves both disk space savings and faster installations.
Strict Package Management
Unlike npm, which you usually use, have you noticed that the structure under node_modules is quite unusual? pnpm does not adopt the flat node_modules structure used by npm and yarn. For reference, here is the node_modules structure generated by npm:
node_modules/
├── body-parser
│ ├── index.js
│ └── package.json
├── express
│ ├── index.js
│ └── package.json
As shown above, npm's node_modules places all packages directly at the root.
On the other hand, with pnpm, only the symbolic link for express is placed directly under node_modules. This is because only express is listed in the dependencies of package.json. This structure means that express is the only package that the application code can access directly.
This is what makes pnpm "strict".
Let's perform an experiment to prove pnpm's strictness. First, create the following code in a project where express was installed using npm.
// npm-repo/index.mjs
import bodyParser from "body-parser";
import assert from "node:assert/strict";
assert.ok(bodyParser);
Even though body-parser is not a package you installed directly, you can successfully import it and run this code without any issues. With a flat node_modules structure, you can access packages that express depends on, even if you haven't installed them directly yourself.
However, this behavior can cause problems in the following cases:
-
body-parserunderwent a major version update during a patch update ofexpress.- For example, suppose the version of
body-parseryou are currently using is 1.x, your code is written based on that assumption, andbody-parserversion 2.x is released. It won't be a problem for a while, but let's sayexpressis updated in a patch release to usebody-parser2.x. Since it's a patch update, there should be no breaking changes inexpressitself, but from the nextnpm installonwards,body-parser2.0 will be placed innode_modules. This could potentially break code that depends onbody-parsereven though you haven't changed anything.
- For example, suppose the version of
-
expressno longer depends onbody-parserafter a patch update.- Similarly to the previous example, if
expressno longer depends onbody-parser,body-parserwill not be placed innode_modules. In this case, code depending onbody-parserwill break again.
- Similarly to the previous example, if
The only way to prevent such issues is to explicitly install and use body-parser as well.
Now, what about the case with pnpm? Let's run the exact same code.
// pnpm-repo/index.mjs
import bodyParser from "body-parser";
import assert from "node:assert/strict";
assert.ok(bodyParser);
As a result, an error is displayed stating that the module does not exist. This is because body-parser is not listed in the dependencies of package.json, so it is not placed directly under node_modules, but exists under node_modules/.pnpm instead.
$ node index.mjs
node:internal/errors:465
ErrorCaptureStackTrace(err);
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'body-parser' imported from /work/pnpm-test/pnpm-repo/index.mjs
at new NodeError (node:internal/errors:372:5)
at packageResolve (node:internal/modules/esm/resolve:954:9)
at moduleResolve (node:internal/modules/esm/resolve:1003:20)
at defaultResolve (node:internal/modules/esm/resolve:1218:11)
at ESMLoader.resolve (node:internal/modules/esm/loader:580:30)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:294:18)
at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:80:40)
at link (node:internal/modules/esm/module_job:78:36) {
code: 'ERR_MODULE_NOT_FOUND'
}
The correct way to resolve this is to simply install body-parser.
$ pnpm add body-parser
Performance
There are benchmarks for pnpm compared to npm, yarn, and Yarn PnP. Looking at these results, it's clear that pnpm can indeed install packages very quickly.
| action | cache | lockfile | node_modules | npm | pnpm | Yarn | Yarn PnP |
|---|---|---|---|---|---|---|---|
| install | 35.3s | 15.7s | 16.7s | 22.9s | |||
| install | ✔ | ✔ | ✔ | 1.8s | 1.1s | 2.1s | n/a |
| install | ✔ | ✔ | 10.3s | 4.1s | 6.5s | 1.42 | |
| install | ✔ | 14.9s | 7.2s | 11.1s | 6.1s | ||
| install | ✔ | 16.8s | 12.6s | 11.5s | 17.2s | ||
| install | ✔ | ✔ | 2.4s | 2.7s | 6.9s | n/a | |
| install | ✔ | ✔ | 1.8s | 1.2s | 7.1s | n/a | |
| install | ✔ | 2.3s | 7.8s | 11.7s | n/a | ||
| update | n/a | n/a | n/a | 1.9s | 9s | 15.4s | 28.3s |
As for why pnpm performs better than other package managers, it is stated that pnpm does not cause blocking between installation stages.
To install a package, there are three stages: "Resolving", "Fetching", and "Writing". As shown in the image below, traditional package managers require each package to wait for the current stage to complete before starting the next stage.

In pnpm, stages are executed individually for each package, so there is no time wasted waiting for other packages to start their next stage.

You can also check the benchmarks published by Yarn. pnpm shows good results there as well.

Impressions
A key characteristic of pnpm is that its node_modules structure is significantly different from other package managers. The fact that a package manager can provide such strict management is an interesting point.
Discussion