pnpm vs Bun vs npm: Package Manager Performance 2026
TL;DR
pnpm is the 2026 default for serious JavaScript projects — content-addressable store, strict dependency isolation, and the best monorepo support. Bun is 5-10x faster than pnpm on installs but still has edge cases with niche packages. npm is the default that works everywhere but is the slowest. For new projects: pnpm (or Bun if you're already in the Bun ecosystem). For CI speed: Bun's install is often faster than even pnpm's cached install.
Key Takeaways
- Bun install: 5-10x faster than pnpm, 15-25x faster than npm (measured on real projects)
- pnpm: Strictest isolation (prevents phantom dependencies), best workspace support, most compatible
- npm: Default, slowest, but universally compatible,
node_modulesphantom deps allowed - Disk usage: pnpm uses ~50% less disk space vs npm (content-addressable store deduplication)
- Monorepos: pnpm workspaces > Bun workspaces > npm workspaces (feature parity gap)
- 2026 recommendation: pnpm for serious projects; Bun install if on Bun runtime already
Downloads / Usage
| Package Manager | Weekly Downloads | Trend |
|---|---|---|
npm | Default (Node.js) | → Stable |
pnpm | ~7M downloads/week | ↑ Growing |
bun | ~1.5M downloads/week | ↑ Fast growing |
Install Speed Benchmarks
Benchmark: Next.js 15 project (1,847 packages)
Environment: M3 MacBook Pro, SSD, cold/warm cache
COLD INSTALL (no cache, no lockfile):
npm: 82s
pnpm: 31s (2.6x faster than npm)
Bun: 8s (10x faster than npm)
CACHED INSTALL (lockfile present, store exists):
npm: 45s (reads node_modules hash)
pnpm: 4s (hardlinks from content store)
Bun: 0.8s (binary cache, near-instant)
CI INSTALL (lockfile present, fresh machine):
npm: 62s
pnpm: 18s (3.4x faster)
Bun: 6s (10x faster)
pnpm: The Recommended Default
# Install pnpm:
npm install -g pnpm
# Or via Corepack (Node.js built-in):
corepack enable pnpm
# Common commands:
pnpm install # Install from lockfile
pnpm add react # Add dependency
pnpm add -D typescript # Add dev dependency
pnpm remove lodash # Remove package
pnpm update --interactive # Interactive update UI
pnpm why lodash # Why is this installed?
pnpm ls # List installed packages
# .npmrc — pnpm configuration:
# Enforce strict peer dependencies:
strict-peer-dependencies=true
# Hoist patterns (allow certain phantom deps for compat):
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
# Save exact versions:
save-exact=true
# Node linker (for compatibility with some tools):
# node-linker=hoisted # Falls back to npm-style if needed
// pnpm-workspace.yaml — monorepo config:
{
"packages": [
"apps/*",
"packages/*",
"tools/*"
]
}
# pnpm workspace commands:
pnpm --filter web add react-query # Add to specific package
pnpm --filter "!web" install # Install all except web
pnpm -r run build # Run build in all packages
pnpm --filter web... run build # Build web + its dependencies
pnpm --filter ...web run build # Build packages that depend on web
Why pnpm Over npm
pnpm advantages:
→ No phantom dependencies (package.json must declare everything)
→ 50% less disk usage (hardlinks, not copies)
→ 3-5x faster installs than npm
→ Best workspace support (filtering, recursive)
→ Isolated node_modules (each package sees only its deps)
pnpm limitations:
→ Occasional compatibility issues with poorly-written packages
→ Slightly steeper learning curve for teams migrating from npm
→ Some tools (older ones) expect hoisted node_modules
Bun: When Speed Is Everything
# Install Bun:
curl -fsSL https://bun.sh/install | bash
# Bun install commands (compatible with npm syntax):
bun install # Install from lockfile
bun add react # Add dependency
bun add -d typescript # Add dev dependency (note: -d not -D)
bun remove lodash # Remove
bun update # Update all packages
# bun.lock — Bun's lockfile format:
# Binary lockfile (bun.lockb) in older versions
# Text lockfile (bun.lock) in Bun 1.1+
# Commit bun.lock to version control
# bunfig.toml — Bun configuration:
[install]
# Use a private registry:
registry = "https://registry.npmjs.org"
exact = true # Pin exact versions
[install.scopes]
# Scoped registry:
"@mycompany" = { token = "$NPM_TOKEN", url = "https://npm.mycompany.com" }
# Bun workspaces:
# package.json at root:
# {
# "workspaces": ["apps/*", "packages/*"]
# }
bun install # Installs all workspaces
bun add react --workspace apps/web # Add to specific workspace
bun run --filter '*' build # Run build in all workspaces
Bun Install Limitations
Known compatibility issues in 2026:
→ Some native binaries may not install correctly
→ Postinstall scripts: some packages assume npm/node environment
→ pnpm-specific workspace.yaml not supported (use package.json workspaces)
→ Some packages with complex resolution logic may resolve differently
Test your project before switching to Bun install in CI:
bun install && bun test # Quick compatibility check
npm: Universal Compatibility
# npm — the universal fallback:
npm install # Install
npm install react # Add
npm install -D typescript # Add dev
npm uninstall lodash # Remove
npm update # Update
# npm workspaces (basic):
# package.json: { "workspaces": ["apps/*", "packages/*"] }
npm install # Installs all workspaces
npm run build --workspace=apps/web # Run in specific workspace
npm run build --workspaces # Run in all workspaces
Corepack: Managing Package Managers
// package.json — specify exact package manager:
{
"packageManager": "pnpm@9.15.0"
}
# Enable Corepack (Node.js 16+):
corepack enable
# Now the packageManager field is enforced:
# If you run npm install in a pnpm project, Corepack intercepts:
# "This project requires pnpm@9.15.0. Run 'corepack use pnpm@9.15.0' to switch."
# In CI — enable Corepack before install:
corepack enable
# Then just run: pnpm install (or whatever packageManager specifies)
Decision Guide
Use pnpm if:
→ New project, want best practices
→ Monorepo with multiple packages
→ Strict dependency isolation important
→ Most compatible choice that's still fast
Use Bun (install) if:
→ Already using Bun as runtime
→ CI speed is critical and you've tested compatibility
→ Greenfield project with modern packages only
Use npm if:
→ Maximum compatibility needed (legacy projects)
→ Required by tooling that expects npm conventions
→ Team unfamiliar with pnpm/Bun
→ Deploying to environment where only npm is available
Lockfile Formats and Reproducible Installs
Each package manager generates its own lockfile format, and the differences matter more than they first appear. npm writes package-lock.json — a JSON file that records exact resolved versions, integrity hashes, and the full dependency tree. It's verbose (often 10,000+ lines for large projects) but fully auditable and diff-friendly. pnpm generates pnpm-lock.yaml in YAML format, which is generally more readable and about 30-40% smaller than npm's equivalent. The YAML format also diffs more cleanly in pull requests since repeated packages aren't duplicated across the file.
Bun's lockfile story changed significantly with Bun 1.1+: the old binary bun.lockb format (fast but unreadable in diffs) was replaced by a text-based bun.lock format. This makes code review meaningful again — reviewers can actually see what changed when a package is added or updated. All three lockfiles serve the same fundamental purpose (reproducible installs), but pnpm's format has the best balance of compactness and readability.
One underappreciated feature of pnpm lockfiles: they record the peerDependencies resolution explicitly, making peer dependency conflicts surfaceable in CI before they manifest as runtime errors. This is one reason large enterprise monorepos tend to gravitate toward pnpm over time.
For CI reproducibility: all three managers respect their lockfile by default when it exists (npm ci for npm, pnpm install --frozen-lockfile for pnpm, bun install --frozen-lockfile for Bun). Pin these flags in your CI workflow to catch lockfile drift early.
Monorepo Workspace Features in Depth
The gap between pnpm, Bun, and npm workspaces becomes most visible in large monorepos. pnpm workspaces are the most mature: the --filter flag supports package name globs, dependency graph traversal (...packageName to select all dependents, packageName... for all dependencies), and changed-package detection via --filter "[origin/main]" which only runs commands in packages that have changed since a git ref. This makes incremental CI pipelines straightforward without additional tooling.
pnpm also enforces strict isolation by default — each package in the workspace can only import packages listed in its own package.json. This prevents the "works locally, breaks in production" class of bug where a package accidentally relies on a hoisted dependency it doesn't declare. The public-hoist-pattern setting in .npmrc lets you selectively hoist specific packages (like ESLint plugins) when tooling compatibility requires it.
Bun workspaces use the npm-compatible "workspaces" field in package.json. The bun run --filter command is functional but lacks pnpm's graph-aware filtering — you can't easily say "run tests only in packages that depend on the package I just changed." For teams evaluating Bun for monorepos, this is the most significant practical gap versus pnpm in 2026.
npm workspaces are the most limited of the three. While they've improved with each Node.js release, features like running scripts in dependency order, filtering by changed packages, and cross-workspace linking are either absent or require manual scripting. Teams using npm workspaces in large monorepos typically add Turborepo or Nx on top to fill these gaps.
Migrating Between Package Managers
Switching between package managers is safer than most teams assume, but there are gotchas. The core migration steps for npm → pnpm: delete node_modules and package-lock.json, run pnpm import (converts npm lockfile to pnpm-lock.yaml), then pnpm install. The import command preserves your exact resolved versions, making the migration non-destructive.
The most common pnpm migration issue is phantom dependency breakage: code that worked with npm because it could reach hoisted packages will fail with pnpm's strict isolation. The fix is adding the missing packages to package.json explicitly — which is the correct behavior anyway. Use pnpm why <package> to understand why any given package is installed and whether it needs to be declared.
For pnpm → Bun migrations, the main risk is packages with complex postinstall scripts or native binaries. Run bun install in a test branch and verify bun test passes before switching CI. Bun reads the same package.json workspaces config, so workspace structure doesn't need to change. One practical tip: keep pnpm-lock.yaml around until you've verified Bun stability in your project, since reverting is trivial.
Compare package manager downloads on PkgPulse.
Compare Bun and pnpm package health on PkgPulse.
When to Use Each in 2026
Use pnpm if:
- You are managing a monorepo and need workspace support that prevents phantom dependencies
- You want significantly faster installs than npm with proven production reliability
- Your team has Node.js-based tooling and you want zero runtime changes
- You work in a corporate environment where Corepack + pnpm is the mandated standard
- You need predictable, reproducible installs with a locked store
Use Bun if:
- You want the fastest possible install speeds and are willing to adopt Bun as your runtime
- You are starting a new project and have no legacy Node.js compatibility constraints
- You want a single tool that handles runtime, bundler, test runner, and package manager
- Your CI runs on Linux x64 where Bun's performance advantage is most pronounced
Use npm if:
- You need maximum compatibility (guaranteed to work with every Node.js tool)
- You are maintaining a legacy project where switching has no payoff
- You are contributing to an open source project that uses npm as the lowest-common-denominator
- You are in a corporate environment where security teams have not approved pnpm or Bun
In practice, pnpm is the most popular alternative in 2026 because it offers meaningful performance improvements over npm without requiring a runtime change. Bun is the high-performance option for greenfield projects willing to commit to the Bun ecosystem.
Security and Supply Chain Considerations
Package manager choice has subtle but real security implications that go beyond install speed. pnpm's content-addressable store provides an interesting property: once a specific version of a package is stored, its files are hardlinked rather than copied. If a dependency is tampered with after your initial install (an unlikely but documented supply-chain attack vector), the stored hash will no longer match on the next pnpm install, and the install will fail with an integrity error. This is similar behavior to npm ci's integrity checking but applies to the global store rather than just the project-level node_modules.
Bun's lockfile records SHA-512 integrity hashes for every package, matching npm's approach. The binary lockfile format in older Bun versions made it harder to audit these hashes in code review, but the text-based bun.lock format addresses this. For security-sensitive projects, pinning exact versions in package.json (using save-exact=true in .npmrc for pnpm, or exact = true in bunfig.toml) is more important than which package manager you use — exact pins prevent floating updates from silently introducing changed code between lockfile regenerations.
See also: Bun vs Vite and pnpm vs Bun vs npm: Package Managers 2026, Bun vs Node.js npm: Runtime Speed & Package Install Benchmarks 2026.