Skip to main content

pnpm vs npm vs Yarn: Package Manager Guide 2026

·PkgPulse Team
0

TL;DR

pnpm is now the best default for new JavaScript projects. It's faster than npm and Yarn Classic, uses significantly less disk space via content-addressable storage, and handles monorepos better out of the box. npm remains fine for simple projects and has the lowest learning curve. Yarn Berry (v4) with PnP is powerful but requires buy-in to its non-standard module resolution. For teams: pnpm. For solo developers new to JS: npm. For existing Yarn Classic users: migrate to pnpm rather than upgrading to Yarn Berry.

Key Takeaways

  • Speed: pnpm ~3x faster than npm on cold installs; ~5x faster with warm cache
  • Disk: pnpm global store deduplicates packages — saves GBs on multi-project machines
  • Monorepo: pnpm workspaces are best-in-class; Yarn workspaces are solid; npm workspaces lag
  • Compatibility: npm = maximum; pnpm ~98% (rare hoisting issues); Yarn PnP requires migration
  • CI: pnpm with --frozen-lockfile is the fastest CI install option

How Each Works Under the Hood

npm:
  node_modules/
    react/           ← full copy
    react-dom/       ← full copy
    lodash/          ← full copy
  Every project: full copy of all packages in its node_modules
  10 projects × 50MB each = 500MB total disk usage
  Simple, well-understood, maximum compatibility

pnpm:
  ~/.pnpm-store/           ← global content-addressable store
    v3/files/
      00/abc123...         ← one copy of each unique file, by hash
      01/def456...

  node_modules/
    .pnpm/
      react@19.1.0/        ← hard link to global store
      react-dom@19.1.0/
    react → .pnpm/react    ← symlink
    react-dom → .pnpm/     ← symlink
  10 projects × same deps = 1 copy in global store
  10 projects = ~50MB total (plus small symlink overhead)

Yarn Classic (v1):
  Similar to npm with better deduplication
  Still the default in many legacy projects
  No longer actively developed beyond security patches

Yarn Berry (v2-v4):
  Option 1: node_modules (same as npm but faster)
  Option 2: Plug'n'Play (PnP) — no node_modules at all!
    .yarn/cache/    ← zip archives of all packages
    .pnp.cjs        ← resolution map (tells Node.js where packages are)
  Maximum speed, zero disk duplication, but requires PnP support

Speed Benchmarks

# Benchmark: installing a Next.js project (162 packages)
# Measured on M2 MacBook Pro, averaged over 3 runs

# Cold install (no cache):
npm install --prefer-offline          → 45.3s
yarn install (classic)                → 38.7s
pnpm install                          → 14.2s   🏆
yarn install (berry, node-modules)    → 28.1s
yarn install (berry, PnP)             → 11.8s   (if PnP compatible)

# Warm install (cache hit, lockfile unchanged):
npm install                           → 8.2s
yarn install (classic)                → 5.1s
pnpm install --frozen-lockfile        → 1.8s    🏆
yarn install (berry, node-modules)    → 3.4s
yarn install (berry, PnP)             → 1.2s

# CI install (with restored cache):
npm ci                                → 12.4s
yarn install --frozen-lockfile        → 9.7s
pnpm install --frozen-lockfile        → 3.1s    🏆

# Why pnpm is faster:
# → Hard links: copying a hard link = creating pointer, not copying bytes
# → Parallel resolution: processes dependencies concurrently
# → Better cache utilization: global store is project-agnostic
# → Stricter lockfile: fewer dependency recalculations

Monorepo Support

# pnpm workspaces (pnpm-workspace.yaml):
packages:
  - 'apps/*'
  - 'packages/*'

# Install all dependencies:
pnpm install  # installs for all workspaces

# Run command in specific workspace:
pnpm --filter @myapp/web build
pnpm --filter @myapp/web... build  # build + all deps

# Run command in all workspaces:
pnpm -r build

# Add dependency to specific workspace:
pnpm --filter @myapp/web add react

# Add shared dependency to root:
pnpm add -w typescript -D

# The key pnpm monorepo advantage:
# Strict isolation — packages can only access what they declare as dependencies
# npm/Yarn hoist everything to root → phantom dependencies (you use X without declaring it)
# pnpm's strict mode catches this at install time, not runtime
// npm workspaces (package.json):
{
  "workspaces": ["apps/*", "packages/*"]
}
// npm workspaces work but have weaker tooling than pnpm
// No --filter with dependency graph awareness
// Phantom dependency problem unaddressed

// Yarn workspaces (package.json):
{
  "workspaces": ["apps/*", "packages/*"]
}
// Yarn's workspace tooling is good but less mature than pnpm's
// yarn workspace @myapp/web run build — syntax is more verbose
// No equivalent to pnpm's ... (dependency graph traversal)

Migrating to pnpm

# From npm:
# 1. Remove node_modules and package-lock.json
rm -rf node_modules package-lock.json

# 2. Install pnpm (once per machine)
npm install -g pnpm
# OR: corepack enable && corepack prepare pnpm@latest --activate

# 3. Import npm lockfile (creates pnpm-lock.yaml from package-lock.json)
pnpm import

# 4. Install
pnpm install

# 5. Update scripts (optional — pnpm run works same as npm run)
# package.json scripts don't need changes
# CI: replace "npm install" with "pnpm install --frozen-lockfile"
# CI: replace "npm run build" with "pnpm build"

# From Yarn Classic:
rm -rf node_modules yarn.lock
pnpm import  # can import yarn.lock too
pnpm install

# Corepack (Node.js 16.9+ built-in):
# package.json:
{
  "packageManager": "pnpm@9.x.x"
}
# Now "pnpm" commands automatically use the declared version
# Ensures team uses same version without global install

When Each Package Manager Makes Sense

npm:
→ Teaching/learning JavaScript (it's already installed with Node.js)
→ Simple scripts and tools that don't need speed
→ Maximum compatibility (some packages still have hoisting requirements)
→ You just want something that works without thinking about it
→ Your CI already caches npm efficiently

pnpm:
→ Any professional project — the speed and disk savings are worth it
→ Monorepos — best workspace tooling
→ Machine with many JS projects (saves GBs in global store)
→ Teams that want strict dependency isolation (no phantom dependencies)
→ Fast CI is a priority

Yarn Classic (v1):
→ Existing projects that work well — don't fix what isn't broken
→ Do NOT start new projects on Yarn Classic (unsupported beyond security)

Yarn Berry (v4) with node-modules mode:
→ If your team is already on Yarn and wants the newer version
→ The PnP migration is not worth it for most teams

Yarn Berry (v4) with PnP:
→ Zero-install workflows (commit .yarn/cache to git, no install step)
→ Maximum reproducibility at the cost of PnP compatibility work
→ Large teams where install determinism is worth the DX cost
→ Not recommended unless you have a specific use case for PnP

The 2026 default recommendation:
→ New project: pnpm
→ Existing npm project: migrate to pnpm when convenient
→ Existing Yarn Classic: migrate to pnpm (not Yarn Berry — different ecosystem)
→ Turborepo/Nx users: pnpm is the recommended package manager for both

Phantom Dependencies and Strict Isolation

The most significant structural difference between npm/Yarn and pnpm is how they handle the node_modules layout. npm and Yarn Classic hoist all packages to the root node_modules directory, which means your code can require('some-transitive-dep') even if you never listed it as a dependency. This is known as the phantom dependency problem: your code works today because a transitive dependency happens to be hoisted, but breaks tomorrow when that transitive dep is removed or upgraded to a version that no longer exports the API you relied on. This is a real source of subtle production breakages that are difficult to debug because the broken code appears to be using an installed package.

pnpm's symlink-based layout prevents phantom dependencies by design. Your project's node_modules contains only symlinks to packages explicitly listed in your package.json. If you try to import a transitive dependency directly, Node.js's module resolution follows the symlink into the .pnpm/ directory and then fails when it cannot find the package in your project's declared dependency list. In practice, this strict isolation surfaces real phantom dependency bugs during pnpm install or on first run — which is exactly when you want to find them, before they cause mysterious failures in production when a dependency tree changes.

For teams maintaining large codebases with dozens of contributors, pnpm's strict mode (shamefully-hoist=false in .npmrc, which is the default) pays dividends over time. New developers who add an import of a package they assume is installed (because it appears in node_modules) get an immediate module not found error rather than code that silently works until a dependency upgrade breaks it six months later. Migration from npm to pnpm occasionally surfaces existing phantom dependency usage — this is a feature, not a bug, and each surfaced phantom dependency is a real issue that would have caused a production incident on a future dependency upgrade.

Lockfile Formats and Determinism

A lockfile's job is to ensure every developer on a team and every CI run installs the exact same dependency tree. All three package managers produce lockfiles, but their designs differ in ways that matter at scale.

npm's package-lock.json is verbose — a typical Next.js project produces a 25,000+ line lockfile. It stores the resolved URL, integrity hash, and full dependency sub-tree for every package. This thoroughness makes it highly deterministic but creates large diffs on every npm install that adds or updates anything. The format uses a v3 lockfile format (npm v7+) that records both the flat and nested representations.

pnpm's pnpm-lock.yaml is more compact. It records resolved versions and integrity hashes but uses YAML's anchors to deduplicate repeated entries. The result is a lockfile that is typically 40–60% smaller than package-lock.json for the same project, with cleaner diffs on updates. The --frozen-lockfile flag (analogous to npm ci) fails the install if the lockfile is out of sync with package.json, making it safe for CI use.

Yarn's lockfile format differs by version. Yarn Classic produces a custom .yarn.lock format that is human-readable but not YAML or JSON — it cannot be parsed by standard tools. Yarn Berry (v2+) uses a similar custom format with optional checksums. One Yarn Berry feature worth noting: Zero-Installs. With PnP mode, teams can commit the .yarn/cache/ directory to the repository, allowing yarn install to skip network requests entirely on CI. This trades repository size for install-step elimination — a worthwhile tradeoff for some enterprise teams.


Security and Provenance Considerations

Package manager security has become a meaningful differentiator in 2026 as supply-chain attacks have increased. All three managers verify package integrity via SHA-512 hashes stored in the lockfile, but their additional security features diverge.

pnpm's strict hoisting model provides an indirect security benefit: because packages can only access their declared dependencies, a compromised transitive dependency cannot accidentally reach production code that never imported it. With npm's flat node_modules, a vulnerability in a deeply nested package is accessible to any code running in the process even if it was never an intentional dependency.

npm itself introduced npm audit and later npm audit signatures (verifying registry signature on published packages) and provenance attestation support. When a package is published through GitHub Actions with npm's provenance feature enabled, npm install can verify the package was built from a specific commit in a specific repository. This is opt-in per package but increasingly common for high-profile packages.

pnpm and Yarn also respect npm audit data (queried from the npm registry's advisory database), so the audit ecosystem works across all three managers. For teams running in restricted corporate environments, pnpm's --registry flag and per-package registry overrides in .npmrc allow routing private packages through internal registries while pulling public packages from the npm CDN.


Workspace Protocol and Dependency Linking

pnpm's workspace: protocol is a significant monorepo feature that has no direct equivalent in npm or Yarn Classic. When a package in your monorepo depends on a sibling package, you declare it with "@company/ui": "workspace:*" in package.json. pnpm resolves this to the actual package at install time and creates a symlink in node_modules pointing directly to the sibling package's source directory — not to a copy or a tarball. This means changes to @company/ui source code are immediately reflected in consuming packages during development, without any build or publish step. The workspace:^ variant pins to the current version range (useful for released packages that use semantic versioning internally), while workspace:* always resolves to whatever is present in the workspace.

npm workspaces support a similar file: protocol but without the nuanced version constraint syntax. Yarn Berry has its own workspace: protocol that works comparably to pnpm's. The practical difference emerges in CI: pnpm's workspace resolution is faster and produces a more compact pnpm-lock.yaml for monorepos with many internal dependencies, because pnpm can represent the workspace link as a single reference rather than duplicating version metadata for each consuming package. For monorepos with 20+ packages that all reference a shared @company/design-system package, pnpm's workspace protocol reduces lockfile size and install time noticeably compared to npm's equivalent configuration.

Corepack and Version Management

Node.js 16.9 shipped Corepack as a built-in tool that manages package manager versions declaratively. The packageManager field in package.json — for example "packageManager": "pnpm@9.12.0" — tells Corepack which version to activate when you run pnpm in that project directory. This eliminates the "works on my machine" problem caused by team members running different package manager versions.

In practice, enabling Corepack with corepack enable makes it the enforcer for all three managers. Running yarn in a pnpm project or npm install in a Yarn project will prompt Corepack to warn or refuse, depending on configuration. CI pipelines should pin the Node.js version and run corepack enable before any install step to guarantee consistency.

For teams managing many projects simultaneously, tools like volta and fnm integrate well with pnpm and npm. pnpm also has its own global bin shim strategy that avoids conflicts when multiple projects use different pnpm versions. The pnpm env use --global lts command manages Node.js versions as well, making pnpm a partial replacement for nvm in some workflows.

Compare pnpm, npm, Yarn, and other JavaScript tooling at PkgPulse.

See also: pnpm vs npm vs Yarn: Package Managers 2026 and Best JavaScript Package Managers 2026, How to Choose Between npm, pnpm, and Yarn in 2026.

See the live comparison

View pnpm vs. npm on PkgPulse →

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.