Every JavaScript project needs a package manager — and for years the choice was trivial (npm). Today you have three serious contenders: npm (the default), Yarn (Facebook's challenger), and pnpm (the disk-space champion). All three install packages from the npm registry, all three manage node_modules, but they make very different trade-offs. This guide tells you exactly what those trade-offs are and when each one wins.
At a glance
| npm | Yarn (Classic & Berry) | pnpm | |
|---|---|---|---|
| Creator | npm, Inc. (2010) | Meta/Facebook (2016) | Zoltan Kochan (2017) |
| Current version | v10+ | Classic v1 / Berry v4+ | v9+ |
| Lockfile | package-lock.json |
yarn.lock |
pnpm-lock.yaml |
| Install speed | Moderate | Fast (Classic) / Fast (Berry) | Fastest |
| Disk usage | High (duplicated) | High (Classic) / Low (Berry PnP) | Very low (hard links) |
| node_modules structure | Flat (hoisted) | Flat (hoisted) | Non-flat (isolated, symlinks) |
| Workspaces (monorepo) | ✅ (npm 7+) | ✅ (first to add) | ✅ (best-in-class) |
| Phantom dependencies | ❌ (allows) | ❌ (Classic) / ✅ (Berry PnP) | ✅ (blocks by default) |
| Zero-installs | ❌ | ✅ (Berry PnP) | ❌ |
| Built into Node.js | ✅ | ❌ | ❌ |
Brief history
npm shipped with Node.js 0.6.3 in 2011. For five years it was the only option. The node_modules folder became a running joke — a black hole heavier than the sun — but it worked.
Yarn Classic (v1) launched in 2016. Meta needed reproducible, fast installs across thousands of developers. Yarn introduced the lockfile concept (npm had none at the time), parallel downloads, and an offline cache. It was dramatically faster and the JS community adopted it quickly.
pnpm appeared in 2017 with a radical idea: don't duplicate packages. Instead, keep one content-addressable store on disk and use hard links (and symlinks) into each project. Suddenly node_modules shrank by 50–90%.
Yarn Berry (v2+) launched in 2020 with Plug'n'Play (PnP) — eliminating node_modules entirely, loading packages from a zip cache via a custom Node.js resolver. Powerful but controversial.
How each one stores packages
npm — flat node_modules
npm hoists all dependencies (and their dependencies) into a single flat node_modules/. This means:
my-project/
└── node_modules/
├── react/ ← your dep
├── loose-envify/ ← react's dep, hoisted up
├── js-tokens/ ← loose-envify's dep, hoisted up
└── ...
Consequence: Your code can require('loose-envify') even though you never declared it — this is the phantom dependency problem.
pnpm — isolated node_modules with a global store
pnpm writes every package version once to a global content-addressable store (~/.pnpm-store). Inside each project it creates a .pnpm folder full of hard links to that store, then symlinks the right entries into node_modules/:
my-project/
└── node_modules/
├── react → .pnpm/react@18.3.1/node_modules/react ← symlink
└── .pnpm/
├── react@18.3.1/
│ └── node_modules/
│ ├── react/ ← hard link to global store
│ └── loose-envify@1.4.0/ ← hard link (react's dep, NOT hoisted)
└── ...
Consequence: Only packages you explicitly depend on are visible at the top level. Phantom dependencies cause an error.
Yarn Berry PnP — no node_modules at all
Yarn Berry patches Node's module resolution to read packages from .yarn/cache/*.zip files using a .pnp.cjs map. There is no node_modules/ directory.
Consequence: Instant installs after the first one (zero-installs possible by committing the cache), but some tools that assume node_modules/ exist can break.
Speed comparison
Install times for a fresh create-react-app-sized project (cold cache → warm cache):
| Scenario | npm | Yarn Classic | pnpm |
|---|---|---|---|
| Cold install (no cache) | ~30s | ~20s | ~15s |
| Warm install (local cache) | ~15s | ~8s | ~3s |
| Adding one package | ~5s | ~3s | ~1s |
node_modules size (React app) |
~340 MB | ~300 MB | ~120 MB (store shared) |
Numbers are approximate benchmarks; your hardware and network matter more than tool differences on cold cache.
pnpm is fastest on warm cache because hard links avoid copying files — it just creates new filesystem entries pointing to already-downloaded data.
Disk usage: the pnpm superpower
If you have 10 projects that all use react@18.3.1:
| Tool | Disk cost |
|---|---|
| npm | 10 × ~7 MB = 70 MB |
| Yarn Classic | 10 × ~7 MB = 70 MB |
| pnpm | 1 × ~7 MB in store + tiny hard links = ~7 MB |
On a machine with dozens of Node.js projects, pnpm can save gigabytes of disk space.
Lockfiles
The lockfile records exact versions (including transitive deps) so that npm install gives every developer identical output.
package-lock.json |
yarn.lock |
pnpm-lock.yaml |
|
|---|---|---|---|
| Format | JSON | Custom text | YAML |
| Readability | Low (huge JSON) | Medium | High |
| Merge conflicts | Common (large JSON diffs) | Moderate | Rare (YAML sections) |
| Committed to git? | ✅ Yes | ✅ Yes | ✅ Yes |
| Reproducibility | ✅ | ✅ | ✅ |
Always commit your lockfile. Never run npm install if your lockfile has been deleted without investigation.
Workspaces (monorepos)
All three support workspaces — the ability to manage multiple packages in one repository.
npm workspaces (package.json):
{
"workspaces": ["packages/*"]
}
npm install # install all packages
npm run build -w packages/ui # run in specific workspace
Yarn workspaces (identical syntax, added in 2017 — npm copied it in v7):
yarn workspace @myapp/ui add react
pnpm workspaces (pnpm-workspace.yaml):
packages:
- "packages/*"
- "apps/*"
pnpm install # installs all
pnpm --filter @myapp/ui build # powerful filtering
pnpm -r test # recursive, all workspaces
pnpm's --filter flag is more powerful than npm/Yarn equivalents — it supports dependency graph traversal:
pnpm --filter "...^@myapp/ui" test # test everything that depends on @myapp/ui
Security
| Feature | npm | Yarn | pnpm |
|---|---|---|---|
| Lockfile integrity | SHA-512 in lockfile | SHA-512 in lockfile | SHA-512 in lockfile |
| Audit command | npm audit |
yarn audit |
pnpm audit |
| Phantom dep protection | ❌ | ❌ (Classic) | ✅ |
| Script execution on install | ✅ (can be blocked) | ✅ (can be blocked) | ✅ (configurable per package) |
| Verdaccio (private registry) | ✅ | ✅ | ✅ |
The phantom dependency protection in pnpm is a security benefit too: a supply-chain attack can't inject a package into your app via an indirect dependency that you didn't explicitly declare.
Compatibility
Most packages work with all three managers. Edge cases:
| Scenario | npm | Yarn | pnpm |
|---|---|---|---|
| Native modules (node-gyp) | ✅ | ✅ | ✅ |
| Postinstall scripts | ✅ | ✅ | ✅ (can disable per package) |
Tools that read node_modules/ directly |
✅ | ✅ (Classic) / ⚠️ (Berry PnP) | ✅ |
require phantom deps |
✅ works (silently) | ✅ (Classic) | ❌ errors (strictness) |
| Docker layer caching | ✅ | ✅ | ✅ (with --frozen-lockfile) |
| Turborepo | ✅ | ✅ | ✅ (recommended) |
| Nx | ✅ | ✅ | ✅ |
Yarn Berry PnP has the most compatibility issues — some tools (Jest, native modules, some Babel configs) need extra configuration. The Yarn team maintains a compatibility table.
Common commands cheat sheet
| Task | npm | Yarn | pnpm |
|---|---|---|---|
| Install all deps | npm install |
yarn |
pnpm install |
| Add package | npm install react |
yarn add react |
pnpm add react |
| Add dev dep | npm install -D vite |
yarn add -D vite |
pnpm add -D vite |
| Remove package | npm uninstall react |
yarn remove react |
pnpm remove react |
| Run script | npm run build |
yarn build |
pnpm build |
| Update deps | npm update |
yarn upgrade |
pnpm update |
| Interactive update | npm install -g npm-check-updates; ncu -u |
yarn upgrade-interactive |
pnpm update --interactive |
| Audit | npm audit |
yarn audit |
pnpm audit |
| Global install | npm install -g typescript |
yarn global add typescript |
pnpm add -g typescript |
| List installed | npm ls |
yarn list |
pnpm list |
| CI/frozen install | npm ci |
yarn install --frozen-lockfile |
pnpm install --frozen-lockfile |
| Exec binary | npx tsc |
yarn dlx tsc |
pnpm dlx tsc |
| Why is this installed? | npm ls react |
yarn why react |
pnpm why react |
Where npm wins
- Zero setup — bundled with Node.js. No install needed.
- Maximum compatibility — every package is tested against npm first.
- Corporate environments — no "why are you using a non-default tool?" questions.
- Simplest mental model — flat
node_modules, one command to learn (npm install). - Most documentation and Stack Overflow answers reference npm.
Where Yarn wins
- Yarn Berry zero-installs — commit
.yarn/cache/and CI never downloads packages. Great for air-gapped or slow-CI environments. - Plug'n'Play strictness — Berry PnP eliminates
node_modulesentirely, catching phantom deps via the resolver layer. yarn upgrade-interactive— the best interactive dependency upgrade UX of the three.- Changesets integration — Yarn Berry + Changesets is the dominant monorepo release workflow.
Where pnpm wins
- Disk space — non-negotiable winner. Saves GBs on developer machines with many projects.
- Install speed (warm) — hard links = near-instant reinstall.
- Phantom dependency protection — strictest without sacrificing
node_modules/compatibility. - Monorepo filtering —
--filterwith dependency graph traversal beats npm/Yarn for large monorepos. - Turborepo's recommended manager — pnpm is the primary recommendation in Turborepo docs.
- Strictness without PnP pain — you get most of Yarn Berry PnP's benefits without the compatibility headaches.
Migration paths
npm → pnpm
# Install pnpm
npm install -g pnpm
# In your project root (pnpm reads package.json and generates pnpm-lock.yaml)
pnpm import # converts package-lock.json → pnpm-lock.yaml
rm package-lock.json
pnpm install
# Tell team members (and CI) to use pnpm
echo "use-pnpm" >> .npmrc # or set packageManager in package.json
npm → Yarn Classic
npm install -g yarn
rm package-lock.json
yarn install # generates yarn.lock
npm → Yarn Berry
npm install -g yarn
yarn set version stable # upgrades to Berry
rm package-lock.json
yarn install # PnP install, no node_modules/
pnpm → npm (rollback)
rm pnpm-lock.yaml
npm install # regenerates package-lock.json
Decision guide
Use npm if:
- You want zero-friction, maximum compatibility
- Your team doesn't know/care about package managers
- You're building a library and want CI to mirror what most users experience
- You're using a managed hosting environment that assumes npm
Use Yarn Classic (v1) if:
- You're maintaining a project that already uses it (no reason to migrate unless pain)
- You want
upgrade-interactiveand slightly faster installs than npm with minimal setup
Use Yarn Berry (v2+) if:
- You need zero-installs for offline/air-gapped CI
- You're building a large monorepo with Changesets for releases
- Your team is experienced and can handle occasional PnP compatibility fixes
Use pnpm if:
- You have many Node.js projects on one machine (disk savings add up fast)
- You're starting a new monorepo (best filter, best speed)
- You want phantom dependency protection without sacrificing
node_modules/compatibility - You're using Turborepo (their recommendation)
- You care about install speed on warm cache
Full comparison table
| Feature | npm | Yarn Classic | Yarn Berry | pnpm |
|---|---|---|---|---|
| Built into Node.js | ✅ | ❌ | ❌ | ❌ |
| Cold install speed | Slow | Fast | Fast | Fast |
| Warm install speed | Medium | Fast | Fast | Fastest |
| Disk usage | High | High | Low (PnP) | Very low |
| Lockfile readability | Low | Medium | Medium | High |
| Phantom dep protection | ❌ | ❌ | ✅ | ✅ |
| Offline cache | Partial | ✅ | ✅ (zero-installs) | ✅ |
| Workspaces | ✅ (v7+) | ✅ | ✅ | ✅ (best) |
node_modules/ present |
✅ | ✅ | ❌ (PnP) | ✅ (symlinked) |
| Compatibility risk | Lowest | Low | Medium-High | Low |
| Turborepo recommendation | — | — | — | ✅ |
| Interactive upgrades | via ncu | ✅ built-in | ✅ built-in | ✅ built-in |
| Global store dedup | ❌ | ❌ | ✅ (zip cache) | ✅ (hard links) |
| Learning curve | Lowest | Low | Medium | Low |
| Community size | Largest | Large | Medium | Growing fast |
Common mistakes
| Mistake | Why it's a problem | Fix |
|---|---|---|
Mixing lockfiles (package-lock.json + yarn.lock) |
Two sources of truth; inconsistent installs | Pick one manager, delete the other lockfile, add engines.packageManager |
Running npm install in a pnpm repo |
Generates package-lock.json, installs differently |
Use only-allow pnpm in package.json scripts |
Committing node_modules/ |
Huge, OS-specific, error-prone | Add to .gitignore (Yarn Berry PnP is the one exception) |
Not using --frozen-lockfile in CI |
CI may silently upgrade transitive deps | Always use npm ci / yarn install --frozen-lockfile / pnpm install --frozen-lockfile |
Ignoring peerDependencies warnings |
Runtime type mismatches, multiple React instances | Read the warnings; install missing peer deps |
Using npm install -g for project tools |
Version mismatch across machines | Add to devDependencies, use npx / pnpm dlx |
| Phantom dependencies (hoisted packages) | Works locally, breaks when dep tree changes | Use pnpm or explicitly declare all deps |
yarn upgrade --latest without review |
Breaks package.json version ranges |
Use upgrade-interactive and review each bump |
FAQ
Which is fastest? pnpm on warm cache (packages already in global store). The difference is negligible on cold CI installs where network is the bottleneck.
Should I migrate from npm to pnpm in an existing project?
Yes, if you have a monorepo or many projects on your machine. Run pnpm import to convert the lockfile — it takes under a minute. The only risk is phantom dependencies surfacing as errors; fix them by explicitly adding those packages.
Is Yarn Berry stable?
Yes, but check the compatibility table for your tools. Jest, Vite, and most modern tools are supported. Some native-module packages need nodeLinker: node-modules in .yarnrc.yml to fall back to classic node_modules/.
Does pnpm work with Docker? Yes. Use the official pnpm Docker pattern:
FROM node:20-alpine
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
What is Corepack? Corepack is a Node.js (v16.9+) tool that lets you pin the package manager version per project:
{
"packageManager": "pnpm@9.1.0"
}
Running pnpm install in this project automatically downloads the pinned pnpm version. It works for npm, Yarn, and pnpm.
Which does Vite/Turborepo/Next.js recommend? Vite: no preference. Turborepo: pnpm (primary examples). Next.js: no preference (shows npm and pnpm examples). Nx: supports all three.