npm vs pnpm vs Yarn 2026: Which to Pick?
TL;DR
pnpm for performance-sensitive teams and monorepos. Yarn Berry for teams with zero-install workflows or complex workspaces. npm for projects that need zero setup or when Node.js compatibility matters most. pnpm's content-addressable store saves gigabytes of disk space and installs 2-3x faster than npm. Yarn Berry's Plug'n'Play mode eliminates node_modules entirely. npm remains the universal default — it's already installed with Node.js.
Quick Comparison
| npm v10 | pnpm v9 | Yarn Berry (v4) | |
|---|---|---|---|
| Weekly Downloads | Built into Node.js | ~18M | ~12M |
| Install Speed | Baseline | 2-3x faster | 1.5-2x faster |
| Disk Usage | High (per-project) | Lowest (shared store) | Medium (compressed cache) |
| Workspaces | Yes (since v7) | Best | Very good |
| Plug'n'Play | No | Optional | Yes (default) |
| Lockfile | package-lock.json | pnpm-lock.yaml | yarn.lock |
| Strict Mode | No | Yes (phantom deps blocked) | Partial (PnP) |
| node_modules | Flat | Symlinked | Optional (PnP = none) |
| Standalone | No (bundled w/ Node) | Corepack | Corepack |
Install Speed and Disk Storage
The performance gap between npm and pnpm is structural, not incremental. npm installs packages into each project's node_modules directory by copying files. If you have 20 projects that all use React 18, npm stores 20 separate copies of React on your disk. Over a developer machine with many projects, this compounds to tens of gigabytes.
pnpm solves this with a content-addressable store in ~/.pnpm-store. Every package version is stored exactly once globally. When you install React in a new project, pnpm creates a hard link from the store to your project's node_modules — the file data is shared, not copied. A hard link is indistinguishable from a regular file at the OS level but occupies no additional disk space.
# Benchmark: installing a mid-size project (Next.js + dependencies)
# Cold cache (no previous install)
npm install # ~45 seconds
pnpm install # ~18 seconds
yarn install # ~28 seconds
# Warm cache (packages already in store/cache)
npm install # ~12 seconds
pnpm install # ~4 seconds
yarn install # ~8 seconds
The speed improvement comes from two factors: the hard-link approach avoids copying file data, and pnpm's dependency resolution algorithm runs more efficiently. It calculates the full dependency graph before starting downloads, then downloads in parallel with fewer redundant resolution steps than npm's hoisting algorithm.
Yarn Berry's speed advantage over npm is real but smaller than pnpm's. In PnP mode, Yarn stores packages as compressed zip archives in .yarn/cache rather than extracting them to node_modules. This eliminates millions of individual file operations (the main bottleneck on both macOS and Windows), making the initial extraction fast and the cache small.
Dependency Resolution: Phantom Dependencies
npm's flat node_modules creates a subtle correctness problem known as phantom dependencies. When you install package A that depends on package B, npm hoists B to the top-level node_modules. Your code can then import B and it will work — even though B is not in your package.json. This is a phantom dependency: you depend on something you haven't declared.
// package.json has: "lodash": "^4.0.0"
// lodash depends on: "some-internal-helper": "^1.0.0"
// npm hoists "some-internal-helper" to node_modules
// Your code:
import helper from 'some-internal-helper'; // WORKS in npm — phantom dependency!
// If lodash drops "some-internal-helper" in a minor update, your code breaks
pnpm blocks this by design. Its node_modules uses symlinks to a virtual store — only packages explicitly in your package.json are accessible at the top level. Attempting to import a transitive dependency that you haven't declared throws a module-not-found error immediately. This is stricter, but it surfaces real dependency hygiene issues that npm silently ignores.
# pnpm strict mode in action
pnpm install lodash
# Attempting to use a transitive dep:
node -e "require('some-internal-helper')"
# Error: Cannot find module 'some-internal-helper'
# pnpm correctly blocks phantom dependency access
Yarn Berry's PnP mode has similar strictness — every package access goes through Yarn's runtime resolver, which validates that the importing package actually declares the dependency. Non-PnP Yarn uses the same hoisting as npm, so phantom dependencies are only blocked in PnP mode.
Monorepo Workspaces
All three package managers support workspaces, but with notable differences in workflow and capabilities.
pnpm workspaces are widely regarded as the best workspace implementation. The pnpm-workspace.yaml file defines which directories are packages. pnpm's strict dependency resolution means each workspace package only sees its declared dependencies, preventing cross-package phantom dependency issues that plague npm monorepos. The --filter flag makes targeted commands concise:
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**'
# Run build only in changed packages
pnpm --filter "...[origin/main]" build
# Run tests in a specific package and its dependencies
pnpm --filter @myorg/ui... test
# Publish all changed packages
pnpm -r publish --filter "...[HEAD~1]"
Yarn workspaces predate pnpm's and have a mature ecosystem of tooling built around them (Turborepo, Nx, and others all support Yarn). Yarn Berry's constraint system lets you enforce workspace policies — like requiring all packages to use the same version of TypeScript — through a constraint file:
// yarn.config.cjs (Yarn Berry constraints)
defineConfig({
async constraints({ Yarn }) {
// All packages must use the same React version
const reactWorkspace = Yarn.workspace({ ident: 'react' });
for (const dep of Yarn.dependencies({ ident: 'react' })) {
dep.update(reactWorkspace.pkg.version);
}
},
});
npm workspaces (added in npm v7) work but are slower and less ergonomic than both alternatives. The --workspace and --workspaces flags are verbose, and npm lacks pnpm's filtering capabilities. For large monorepos, most teams that start with npm workspaces eventually migrate to pnpm.
Zero-Install and CI Caching
Yarn Berry's PnP mode enables "zero-install" repositories: you commit the .yarn/cache directory (compressed package archives) to git. On any machine — including CI — running yarn on a fresh clone produces a fully functioning install without downloading anything from the network. This is controversial (git repos get larger, up to hundreds of MB for large projects) but eliminates CI cache invalidation and network-dependent installs entirely.
# Yarn zero-install setup
yarn config set enableGlobalCache false
yarn config set compressionLevel mixed
# Commit the cache
git add .yarn/cache
git commit -m "Add Yarn zero-install cache"
# CI: no network needed
git clone repo && yarn # Instant, no downloads
pnpm and npm handle CI differently — both rely on a local cache (.pnpm-store or ~/.npm) that CI systems restore between runs. When the cache hits, installs are fast. When it misses (new CI runner, expired cache), the full download runs. pnpm's cache is more compact because packages aren't duplicated across projects.
# GitHub Actions — pnpm with cache
- uses: pnpm/action-setup@v4
with:
version: 9
- name: Get pnpm store directory
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
Migration Paths
npm → pnpm: Drop-in replacement for most projects. Install pnpm globally, run pnpm import to convert package-lock.json to pnpm-lock.yaml, then run pnpm install. The main friction is phantom dependencies: pnpm's strict mode may reveal imports that "worked" under npm because of hoisting. Fix by adding the missing packages to package.json.
# Migration from npm to pnpm
npm install -g pnpm
pnpm import # Converts package-lock.json → pnpm-lock.yaml
pnpm install # Install with pnpm
rm package-lock.json # Remove npm lockfile
npm/Yarn Classic → Yarn Berry: More involved. Yarn Berry has breaking changes from Yarn Classic (v1). PnP mode is incompatible with some packages that rely on node_modules traversal. The nodeLinker: node-modules mode in Berry is compatible with Classic but gives up PnP's benefits.
Package Manager Governance
npm is maintained by GitHub (which Microsoft acquired). It ships with Node.js through the official Node.js installer. This makes it the universal baseline — every Node.js developer already has npm, regardless of platform or setup.
pnpm is an independent open-source project with corporate backing from several companies using it in production. Vercel, Microsoft, and others use pnpm in large monorepo contexts. The maintainers are highly responsive and pnpm's release cadence has been consistent.
Yarn is maintained by Yarn's open-source community with strong contributions from Datadog and others. Yarn Classic (v1) is in maintenance mode — no new features, only security patches. Yarn Berry (v2+, now v4) is the active branch, but it's a significant paradigm shift from v1.
When to Use Which
Choose pnpm when:
- Speed and disk efficiency matter (CI bills, developer machine space)
- You're building or maintaining a monorepo
- You want strict dependency enforcement (no phantom deps)
- You're greenfielding a project and want the best defaults
Choose Yarn Berry when:
- You want zero-install for deterministic offline installs
- Your team already uses Yarn workspaces and doesn't want to migrate
- You need Yarn's constraint system for enforcing monorepo policies
Stay on npm when:
- You need maximum ecosystem compatibility with zero configuration
- You're working with tools or platforms that assume npm (some deployment platforms, legacy tooling)
- You're maintaining an open source project where contributors shouldn't need to install anything extra
The package manager ecosystem has converged significantly — all three support workspaces, lockfiles, and scripting. The decision increasingly comes down to specific team needs: monorepo scale (pnpm wins), zero-install reproducibility (Yarn Berry wins), or zero-configuration simplicity (npm wins). For most new projects starting in 2026, pnpm is the practical default: it's faster, uses less disk space, and its strict mode catches real dependency issues that would otherwise appear as mysterious failures in production.
Compare npm and pnpm package stats on PkgPulse. See also pnpm vs npm vs Yarn package manager performance 2026 and best JavaScript package managers 2026.