Npm ships with Node and just works — that frictionless default is why it still commands the largest install base in 2026. pnpm started as a niche performance project and is now the recommended tool in most new monorepo starters, Vite templates, and framework scaffolders. Understanding the difference saves you from either over-engineering a simple project or leaving real CI cost on the table for a large one.
What changed in 2026
- pnpm 9 is stable and widely adopted — the
node_modules isolation model and workspace filtering are polished enough that migration risk is low for new projects.
- npm 11 improved install speed — the rewrite of the dependency resolution algorithm in npm 10/11 closed the gap with pnpm for small-to-medium trees.
- Corepack ships in Node 22 LTS — pnpm and Yarn can now be installed without a global
npm install -g, reducing the "which manager is installed" friction on CI.
- Yarn Berry (Plug'n'Play) stalled — most teams that evaluated Yarn Berry reverted to npm or moved to pnpm due to PnP compatibility issues. Yarn Classic (1.x) is maintained but not evolved.
Core difference: how packages are stored
npm and Yarn Classic: Each project's node_modules gets its own copy of every package. Ten projects using lodash@4.17.21 = ten copies on disk.
pnpm: A single global content-addressable store (~/.pnpm-store) holds each version once. Projects get hard-linked copies — same bytes, no duplication.
# npm install in a project with 800 dependencies
# Disk: ~300 MB per project
npm install
# pnpm install same project
# Global store: ~300 MB total (shared across all projects on the machine)
pnpm install
On a machine with 20 Node projects, pnpm's store advantage is substantial. On a fresh CI runner with one project, the benefit is smaller.
Performance comparison
| Operation |
npm 11 |
pnpm 9 |
| Cold install (800 deps, no cache) |
~35 s |
~20 s |
| Warm install (lockfile matches cache) |
~6 s |
~2 s |
| Monorepo (20 packages, filtered install) |
~90 s |
~25 s |
| Disk per project (800 deps) |
~280 MB |
~5 MB (hard links) |
| Lockfile format |
package-lock.json |
pnpm-lock.yaml |
Timings are representative benchmarks on a modern CI runner; actual results vary with registry latency and tree shape.
Monorepo workspaces
pnpm workspace filtering is a genuine differentiator:
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
# Install only the packages affected by a change
pnpm install --filter ...@my-org/ui
# Run build only in changed packages and their dependents
pnpm run --filter ...@my-org/api build
# Run tests in parallel across all packages
pnpm run --parallel -r test
npm workspaces support similar syntax but lack --filter graph traversal. For 5+ packages in a monorepo, pnpm's workspace tooling is noticeably better.
Phantom dependency detection
pnpm's strict node_modules layout exposes packages your code imports but never explicitly declared:
# This works with npm (dependency was hoisted)
import { foo } from 'transitive-dep'; // not in your package.json — phantom
# pnpm throws at runtime or install
# Error: Cannot find module 'transitive-dep'
This strictness is a feature, not a bug — phantom dependencies are real bugs waiting to surface after an upstream update.
How to pick
- New project, any size? → pnpm. Zero migration cost, better defaults, Corepack makes it easy.
- Monorepo with 5+ packages? → pnpm. Workspace filtering and performance difference is significant.
- Existing project, stable, small team? → Stay on npm unless CI cost is a pain point.
- Framework that defaults to a specific manager? → Use what the framework recommends to avoid lockfile conflicts.
- Team unfamiliar with pnpm? → One afternoon of orientation is sufficient; the learning curve is shallow.
Common mistakes
Mixing package managers in the same repo. Having both package-lock.json and pnpm-lock.yaml in the same repo causes chaos. Delete one and commit only the winner.
Not setting engine-strict in pnpm. Without it, pnpm installs regardless of the Node version mismatch declared in engines.
// package.json
{
"engines": { "node": ">=20.0.0", "pnpm": ">=9.0.0" },
"packageManager": "pnpm@9.12.0"
}
Using --shamefully-hoist as a default. This flag disables pnpm's isolation and makes it behave like npm — you lose all the phantom-dependency protection. Use it only as a short-term compatibility shim.
What to skip
- Yarn Berry (PnP mode) for new projects in 2026 — the compatibility issues with tools that expect real
node_modules (Jest, certain Vite plugins) have not fully resolved.
- Global npm installs for CLI tools you need in CI — use pnpm with
pnpm exec or add them as dev dependencies for reproducibility.
- Committing
node_modules regardless of manager — use .gitignore and lean on the lockfile.
FAQ
Is pnpm a drop-in replacement for npm?
For most projects, yes. Replace npm install with pnpm install, npm run with pnpm run. The lockfile format changes, so commit pnpm-lock.yaml and delete package-lock.json.
Does pnpm work with Nx and Turborepo?
Yes. Both support pnpm workspaces natively. Turborepo's remote cache and Nx's affected-command detection work alongside pnpm filtering.
What is Corepack and do I need it?
Corepack is a Node built-in that manages package manager versions per project, reading the packageManager field in package.json. It means CI and teammates get the exact pnpm version you specify without a global install step.
Will switching to pnpm break my CI pipeline?
Minimally. Replace npm ci with pnpm install --frozen-lockfile. Most CI providers (GitHub Actions, GitLab CI) have pnpm setup actions or support Corepack natively.
Where to go next