JavaScript Package Managers in 2026: npm, pnpm, Yarn, and Bun
JavaScript Package Managers in 2026: npm, pnpm, Yarn, and Bun
Four package managers, one registry, and a lot of opinions. npm is the default that ships with Node.js. pnpm saves disk space and enforces correctness. Yarn Berry reimagined how dependencies are resolved. Bun treats package management as a feature of the runtime itself. Each makes different trade-offs, and the right choice depends on your project.
Feature Comparison
| Feature | npm (v10+) | pnpm (v9+) | Yarn Berry (v4) | Bun |
|---|---|---|---|---|
| Default with Node.js | Yes | No | No | No (ships with Bun) |
| Lockfile | package-lock.json | pnpm-lock.yaml | yarn.lock | bun.lock |
| node_modules structure | Flat (hoisted) | Symlinked (strict) | Plug'n'Play (no node_modules) | Flat (hoisted) |
| Content-addressable store | No | Yes | No (zip cache) | No |
| Workspace support | Yes | Yes | Yes | Yes |
| Dependency patching | Via patch-package | Built-in | Built-in | No |
| Corepack compatible | Yes | Yes | Yes | No |
Performance Benchmarks
Install times on a mid-size project (~800 dependencies, CI environment):
| Scenario | npm | pnpm | Yarn Berry (PnP) | Bun |
|---|---|---|---|---|
| Clean install (no cache) | 38s | 18s | 14s | 8s |
| Clean install (warm cache) | 22s | 8s | 6s | 4s |
| Adding one dependency | 12s | 4s | 3s | 2s |
| CI install (lockfile, cache) | 18s | 7s | 5s | 3s |
| Disk usage | 420 MB | 290 MB | 45 MB (PnP) | 415 MB |
These numbers are representative, not absolute. The relative ordering is consistent: Bun is fastest, Yarn PnP and pnpm are in the middle, npm is slowest. The disk usage gap is dramatic -- pnpm's content-addressable store keeps one copy per package version across all projects, and Yarn PnP uses compressed zips instead of extracted files.
npm: The Default
npm ships with Node.js. As of v10, it's competent -- not exciting, but competent. Every tutorial, CI template, and deployment guide assumes npm. Zero setup cost, zero onboarding friction.
The problem: npm's flat hoisting means your code can import packages you didn't declare as dependencies. This "phantom dependency" issue causes builds to work locally but fail elsewhere. npm is also the slowest installer by a significant margin.
Use npm when your team has no reason to switch or when you need guaranteed compatibility with every tool. There's nothing wrong with npm -- it's just not the best at anything specific.
pnpm: Correctness and Efficiency
pnpm stores every package version once in a global content-addressable store (~/.local/share/pnpm/store) and hard-links files into each project's node_modules. Install [email protected] in five projects and pnpm stores it once. npm would keep five full copies.
# Install via Corepack (recommended)
corepack enable && corepack prepare pnpm@latest --activate
pnpm install # Install all deps
pnpm add express # Add a package
pnpm's node_modules is non-flat -- each package can only access its declared dependencies. If you import a transitive dependency you didn't declare, pnpm throws a resolution error. This strictness catches real bugs that npm silently hides. When migrating, expect to add a few missing declarations to dependencies. That's pnpm showing you problems that were always there.
# Relax strictness if needed temporarily (.npmrc)
node-linker=hoisted
pnpm has the strongest monorepo workspace support. The --filter flag supports git-based change detection, dependency graph traversal, and glob patterns:
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
pnpm -r run build # Build all packages
pnpm -r --filter "...[origin/main]" run build # Only changed packages
pnpm add zod --filter @myapp/api # Add to specific workspace
Yarn Berry: Plug'n'Play and Zero-Installs
Yarn Berry (v2+, currently v4) made a radical bet: eliminate node_modules entirely. Plug'n'Play (PnP) replaces it with a .pnp.cjs resolution map and compressed zip archives in .yarn/cache/.
corepack enable && corepack prepare yarn@stable --activate
yarn init -2
yarn install
# Result: no node_modules. Instead: .pnp.cjs + .yarn/cache/*.zip
Because the cache is small compressed zips, you can commit it to git. A fresh clone needs no install step -- just yarn to regenerate the PnP map. This eliminates CI install time entirely:
# .yarnrc.yml
nodeLinker: pnp
enableGlobalCache: false
enableImmutableInstalls: true # CI mode
The trade-off: PnP breaks tools that traverse node_modules directly. React Native, some ESLint configs, and tools that shell out to node without the PnP loader have historically had issues. Ecosystem support has improved in 2026, but you'll still hit edge cases with native addons.
Yarn provides a node-modules linker fallback, but at that point you lose PnP's benefits and should consider pnpm instead.
Bun: Speed as a Feature
Bun's package manager is built into the runtime, written in Zig, and optimized for raw throughput. bun install is consistently 5-10x faster than npm and 2-3x faster than pnpm.
bun install # Install deps
bun add express # Add a package
bun add -d vitest # Add dev dependency
bun remove lodash # Remove a package
Bun 1.2+ generates a text-based bun.lock (replacing the earlier binary bun.lockb). It can read package-lock.json, yarn.lock, and pnpm-lock.yaml for painless migration.
The limitations: Bun uses flat hoisting like npm, so phantom dependencies are possible. Workspace support works but lacks pnpm's filtering sophistication. Some packages with complex native compilation steps may fail -- Bun uses a trustedDependencies allowlist to control which packages run lifecycle scripts.
Use Bun when speed is your priority and you're already on the Bun runtime. The install speed advantage is most impactful in CI pipelines.
Monorepo Comparison
| Feature | npm | pnpm | Yarn Berry | Bun |
|---|---|---|---|---|
| Workspace protocol | * |
workspace:* |
workspace:* |
workspace:* |
| Selective execution | --workspace |
--filter |
yarn workspace |
--filter |
| Parallel execution | No | Yes (-r) |
Yes (topological) | Partial |
| Change-based filtering | No | Yes (git-based) | Yes (plugin) | No |
| Hoisting control | Limited | Granular | Granular | Limited |
For serious monorepo usage, pnpm and Yarn Berry are clearly ahead.
Migration Guides
npm to pnpm
corepack enable && corepack prepare pnpm@latest --activate
rm -rf node_modules package-lock.json
pnpm import # Import resolutions from old lockfile
pnpm install # Install; fix any missing dependency declarations
npm/pnpm to Yarn Berry
corepack enable
yarn set version stable
rm -rf node_modules package-lock.json pnpm-lock.yaml
yarn install
# For node_modules fallback instead of PnP:
# echo 'nodeLinker: node-modules' >> .yarnrc.yml && yarn install
Any manager to Bun
bun install # Reads existing lockfiles automatically
# Replace 'npx' with 'bunx' in scripts
Migration Checklist
- Update CI/CD install commands and cache configuration
- Update
Dockerfileif applicable - Pin the package manager version (Corepack or explicit in CI)
- Verify
postinstallscripts still run correctly - Test build, test, and deploy pipelines end-to-end
- Check IDE integrations (Yarn PnP needs SDK setup)
Configuration Cheatsheet
pnpm (.npmrc)
strict-peer-dependencies=true
auto-install-peers=true
public-hoist-pattern[]=@types/*
frozen-lockfile=true
Yarn Berry (.yarnrc.yml)
nodeLinker: pnp
enableImmutableInstalls: true
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
Bun (bunfig.toml)
[install]
frozenLockfile = true
trustedDependencies = ["esbuild", "sharp"]
Recommendations
Solo project or small team: Use pnpm. Correctness guarantees catch real bugs, speed is good, and ecosystem support is broad.
Large monorepo: Use pnpm or Yarn Berry. pnpm's --filter is excellent. Yarn PnP's zero-installs shine if your toolchain supports them. Evaluate both against your specific tools.
Bun runtime project: Use bun install. If Bun is your runtime, its package manager is the path of least resistance.
CI-sensitive project: Use Bun for speed or Yarn Berry zero-installs to skip the install step entirely.
Maximum compatibility: Use npm. Lowest risk, works everywhere, just slower.
Starting fresh with no constraints: Use pnpm. Best balance of speed, correctness, and ecosystem support. It's strict where it should be and flexible where it needs to be. Most teams that switch to pnpm don't switch back.