pnpm: The Package Manager That Actually Respects Your Disk
Deep Dive · Package Managers
pnpm: The Package Manager That Actually Respects Your Disk
How a content-addressable store and hard links turned a 50 GB node_modules problem into a solved one.
~90%less disk space vs npm
3×faster installs (cached)
1copy of each package, ever
0phantom dependency bugs
The node_modules Problem You’ve Learned to Live With
Every JavaScript developer has been here: you open a project folder and it's 300 MB. You have 20 projects. That's 6 GB — mostly duplicates.
npm and Yarn both work the same way at the storage layer: every project gets its own isolated copy of every dependency. **react@18 in project A is a completely separate set of files from react@18 in project B.** Even if both projects use identical versions, the bytes are duplicated on disk twice.
Scale that to a team with a monorepo, a CI server, and a developer laptop with 30 Node projects, and you're managing hundreds of gigabytes of redundant data.
💾
Redundant Storage
Each project downloads and stores its own full copy of every dependency, even if identical versions exist elsewhere on the machine.
🐌
Slow Re-installs
Delete node_modules, run npm install — it downloads everything again. No cross-project reuse of already-downloaded packages.
👻
Phantom Dependencies
npm's flat node_modules lets code require packages it never declared. Works until a transitive dep changes version. Silent time bombs.
🏗️
Monorepo Pain
Sharing packages across workspace packages in a monorepo requires complex hoisting workarounds and still duplicates transitive deps.
The Content-Addressable Store: One Copy, Infinite Projects
pnpm maintains a single global store at `~/.local/share/pnpm/store`. Every package version is stored exactly once. Projects get hard links — not copies.
A **hard link** is a second directory entry pointing to the same bytes on disk. Reading the file from either location is identical — the OS sees one file. When 10 projects all use `react@18.2.0`, there is exactly one physical copy on your disk shared by all ten.
npm / Yarn approach
Project A
react@18 (14MB)
lodash@4 (6MB)
Project B
react@18 (14MB)
lodash@4 (6MB)
Project C
react@18 (14MB)
lodash@4 (6MB)
Total: 60 MB (×3 copies)
VS
pnpm approach
Global
Store
~/.pnpm/store
react@18 (14MB ×1)
lodash@4 (6MB ×1)
Project A
Project B
Project C
hard link
hard link
hard link
Total on disk: 20 MB (shared by all)
npm duplicates packages per-project; pnpm stores each version once and hard-links it everywhere
Because hard links share the same inode, the OS treats them as a single file. Deleting `node_modules` in one project leaves the store and all other projects completely intact. The store entry is only freed when no project references it.
Real-World Disk Savings
The savings compound as you add projects. By 10 projects the difference between npm and pnpm is measured in gigabytes.
1 GB
750 MB
500 MB
250 MB
0
1 project
3 projects
5 projects
10 projects
npm
Yarn
pnpm
200
190
170
600
540
190
1000
900
200
2000+
1800+
220
Disk usage (MB) for projects sharing a common set of dependencies. pnpm barely grows with each new project.
The reason pnpm's bar barely moves: the packages are already in the store. Adding a new project that uses the same dependencies costs only a few kilobytes of hard link entries — not megabytes of package files.
"On a laptop with 20 Node projects, switching from npm to pnpm recovered 18 GB of disk space on the first run."
Installation Speed: Cold Cache vs Warm Cache
pnpm shines most on repeat installs. Once a package is in the global store, installing it into a new project is near-instant — no download, just hard link creation.
Installation Time (lower = faster)
First install (cold cache)
npm
~ 8.2s
Yarn
~ 6.0s
pnpm
~ 3.5s
Re-install (warm cache, packages in store)
npm
~ 5.8s
Yarn
~ 4.1s
pnpm
~ 0.9s
just hard links!
Times are representative benchmarks for a mid-sized app (~50 deps). pnpm's warm-cache install creates hard links instead of copying files.
Why is pnpm's warm-cache so fast? When you delete `node_modules` and re-run `pnpm install`, no package is downloaded. pnpm walks the `package.json`, confirms each version is in the store, and creates hard links. The entire operation is metadata manipulation — the filesystem equivalent of instant.
npm and Yarn must still verify file integrity and re-copy files on each install, even with a populated cache.
Strict Isolation: No Phantom Dependencies
npm and Yarn hoist all packages to a flat `node_modules/`. This means your code can accidentally import a package it never declared — and your CI will explode when that transitive dep disappears.
npm flat node_modules
node_modules/
├── react/
├── lodash/
├── scheduler/ (transitive)
├── prop-types/ (transitive)
├── loose-envify/ (transitive)
└── js-tokens/ (transitive)
// Works! (but it's a trap)
import { debounce } from 'lodash'
⚠ Phantom dep — not in package.json
pnpm strict symlinks
node_modules/
├── react/ → .pnpm/react@18/
└── .pnpm/ (hidden store)
├── react@18/node_modules/
│ ├── scheduler/
│ └── prop-types/
└── lodash@4/ (not declared!)
// Blocked by pnpm
import { debounce } from 'lodash'
✓ ERR: lodash not in package.json
npm exposes transitive deps for accidental use; pnpm's symlink layout makes them invisible to your code
pnpm's `node_modules` only exposes packages you actually declared. Your dependencies' dependencies are nested inside `.pnpm/` where your code cannot reach them. If you accidentally use a transitive package, pnpm will error during install — not during a production deploy six months later when a transitive dep is updated.
First-Class Workspace Support for Monorepos
pnpm's workspace support is built in — no plugins, no Lerna, no Nx required for the basics. Add a `pnpm-workspace.yaml` and all your packages share the same store, link to each other automatically, and run scripts across the entire repo.
`# pnpm-workspace.yaml packages:
-
‘packages/*’
-
‘apps/*’`
Cross-package references use the
workspace:protocol — pnpm links them as hard-linked directory entries during development, then replaces with real version numbers during publish.
# packages/ui/package.json { "dependencies": { "@myapp/utils": "workspace:*" } }
🔗
workspace:* Protocol
Local packages reference each other via symlinks during development. No manual linking, no `npm link` confusion.
🏃
Recursive Scripts
`pnpm -r run build` runs build in every workspace package. Add `--parallel` for concurrent execution.
🔍
Filter by Package
`pnpm --filter @myapp/ui run test` runs only in the packages you specify, with dependency awareness.
Getting Started in 60 Seconds
Install pnpm
`# Via npm (ironic, but it works) npm install -g pnpm
Or via the standalone script (no Node required)
curl -fsSL https://get.pnpm.io/install.sh | sh -
Or with Homebrew
brew install pnpm`
Migrate an Existing Project
`# Remove old lockfile and node_modules rm -rf node_modules package-lock.json yarn.lock
Install with pnpm — it reads your package.json
pnpm install
A pnpm-lock.yaml is generated — commit this`
Drop-In Command Replacements
npm / Yarnpnpm equivalentNotes
`npm install``pnpm install`Reads pnpm-lock.yaml
`npm install react``pnpm add react`Adds to dependencies
`npm install -D eslint``pnpm add -D eslint`Adds to devDependencies
`npm run build``pnpm run build` or `pnpm build`Scripts work identically
`npx create-next-app``pnpm dlx create-next-app`dlx = one-time exec without install
`npm update``pnpm update`Respects semver ranges
`npm outdated``pnpm outdated`Shows what can be updated
Pro Commands Worth Knowing
`# See where the store lives and how big it is pnpm store path
Remove packages no longer referenced by any project
pnpm store prune
Run a script across all workspace packages
pnpm -r run test
Run only in packages that changed since git HEAD
pnpm —filter …[HEAD~1] run build
Show why a package is installed
pnpm why lodash
Interactive update (choose what to bump)
pnpm update —interactive`
pnpm vs npm vs Yarn: Feature Matrix
FeaturenpmYarn Berrypnpm
**Disk efficiency**
Copies per project
Cache, no hard links
Hard links, 1 copy
**Re-install speed**
Re-copies all files
Copies from cache
Links in ms
**Phantom deps**
Allowed (flat)
Blocked (PnP)
Blocked (symlinks)
**Workspace support**
Workspaces (basic)
Workspaces + PnP
workspace:* protocol
**Node.js compat**
Universal
Needs .yarnrc.yml
Drop-in compatible
**lockfile format**
package-lock.json
yarn.lock
pnpm-lock.yaml (readable)
**Learning curve**
None
High (PnP mode)
Very low
Try It Right Now
pnpm is a single install command away and works with every existing Node.js project. No configuration changes needed for basic usage.
npm install -g pnpm pnpm install # drop-in for npm install Your package.json and existing scripts remain unchanged.
Written for developers tired of 50 GB node_modules directories. · pnpm.io · MIT Licensed
Enjoyed this post?
Get the next one in your inbox — only when I ship something worth reading.
Newsletter form not configured.
Or follow on Substack for the newsletter.
Comments via GitHub Discussions
Comments not configured. Set GISCUS env vars to enable.