Why to use pnpm
Sunday 31/07/2022
·5 min readpnpm is the package manager I default to for every Node project, and the reason is more interesting than "it's faster." The marketing pitch is speed and disk savings, but the actual engineering win is the strict node_modules layout — it makes phantom dependencies (modules you import without declaring) impossible to ship by accident. That alone has caught real bugs for me on three different projects. Below: four reasons pnpm is worth switching to, with concrete numbers from a typical Next.js workspace.
1. Content-addressable disk storage
Both npm and yarn keep a separate copy of every package inside every project's node_modules. If you have ten Node projects on your machine, you have ten copies of lodash, react, typescript, and everything else they pull in transitively.
pnpm stores every package version exactly once in a global content-addressable store (~/Library/pnpm/store on macOS, ~/.local/share/pnpm/store on Linux). Each project's node_modules contains hard links — not copies — into that store. On my machine, the store is ~4 GB and saves an estimated 60 GB across the Node projects I work on regularly.
The first install on a fresh machine is similar speed to npm. Every install after that — adding a dependency, switching branches, cloning a sibling project — is dramatically faster because pnpm just creates hard links to packages already on disk.
2. Strict node_modules prevents phantom dependencies
This is the killer feature most pnpm tutorials skip past.
With npm or yarn, every transitive dependency ends up at the top level of node_modules. If your code does import _ from 'lodash', it works even if lodash isn't in your package.json — as long as some other dependency declared it. That's a phantom dependency: your app silently relies on lodash being there, but breaks the moment that other dependency drops it.
pnpm builds a node_modules where only your direct dependencies are accessible at the top level. Everything else lives in node_modules/.pnpm/ and is reached through symlinks. Try to import a phantom dependency and you get a real, immediate Cannot find module error — at install time, in CI, in development. Not in production three months later.
This caught one real bug on a project where we'd been importing date-fns everywhere despite it only being a transitive dependency of a UI library we later replaced. The pnpm migration surfaced 14 import sites that all needed date-fns added to package.json. With npm, we'd have shipped a broken build.
3. Faster installs on real projects
Benchmarks vary by lockfile state, but a rough rule on a Next.js app with ~600 dependencies:
| Scenario | npm | yarn | pnpm | |---|---:|---:|---:| | Cold install (no cache) | ~50s | ~40s | ~25s | | Warm install (lockfile + store) | ~12s | ~10s | ~3s | | Add one dependency | ~8s | ~6s | ~2s |
The cold-install gap is modest. The warm-install gap — what you actually feel day-to-day — is 3–4×. CI build times drop noticeably once you cache pnpm store path between runs.
4. First-class monorepo support
pnpm ships pnpm-workspace.yaml and the --filter flag with no separate tooling. Run pnpm --filter @myapp/web build and only that package and its dependencies are built. Internal packages link through the same content-addressable store, so a change in a shared library is visible across the workspace without a publish step. The equivalent with npm needs lerna or nx; Yarn ships its own workspaces but the filter ergonomics are less polished.
Getting started
# install via corepack (preferred on Node 16.13+)
corepack enable
corepack prepare pnpm@latest --activate
# or globally via npm
npm install -g pnpm
# day-to-day usage
pnpm install # equivalent to npm install
pnpm add lodash # add a dependency
pnpm add -D vitest # add dev dependency
pnpm run build # or just: pnpm build
pnpm dlx create-next-app # one-off package execution, like npx
Most npm commands have a 1:1 pnpm equivalent. The lockfile is pnpm-lock.yaml, replacing package-lock.json.
Gotchas worth knowing
- Some packages misbehave with the strict layout. Older libraries that import undeclared peer dependencies will fail. The fix is usually a
.npmrcwithshamefully-hoist=truefor that project — but treat it as a temporary escape hatch, not a permanent setting. The phantom dependencies it papers over are real bugs. - CI caching needs the store path, not just
node_modules. Cache whateverpnpm store pathreports for the speedups to land. Caching onlynode_modulesmisses most of the win. - Native modules sometimes need a
rebuildstep when switching Node versions.pnpm rebuildis the fix.
FAQ
Is pnpm compatible with packages built for npm?
Yes — pnpm reads package.json and resolves the same npm registry. The only differences are the disk layout and the lockfile format. Any package that works with npm works with pnpm.
Can I use pnpm with Yarn workspaces?
You can't mix lockfiles in the same project — pick one. But pnpm's workspace support is at least as featureful as Yarn's, so migrating off Yarn workspaces is usually straightforward: delete yarn.lock, write a pnpm-workspace.yaml, run pnpm install.
Does pnpm work in CI on GitHub Actions?
Yes — there's an official pnpm/action-setup action. Cache the path returned by pnpm store path to get the full speed advantage.
What's the catch with content-addressable storage?
If you delete the store while a project still references it, the symlinks break. The fix is pnpm install --force to re-link. The store is regenerated on demand — one slow install, not permanent loss.