Four package managers dominate the JavaScript ecosystem: npm (ships with Node), Yarn (Classic and Berry), pnpm (strict, content-addressable), and Bun (runtime + installer). They all read package.json, talk to the npm registry, and produce a lockfile — but they differ in how they lay out node_modules, how they cache packages, and how well they scale monorepos.
This page is the decision matrix. Deep dives live under Yarn, pnpm, Bun, and Corepack.
📝
note
Pick one manager per repo and pin it with packageManager+ Corepack (or an equivalent CI install). Mixing lockfiles is a common source of "works on my machine" bugs.
At a Glance
Dimension
npm
Yarn Berry
pnpm
Bun
Default with Node
Yes
No (Corepack)
No (Corepack)
Separate install
Lockfile
package-lock.json
yarn.lock
pnpm-lock.yaml
bun.lock / bun.lockb
Layout
Hoisted tree
PnP or node_modules
Isolated + store
Hoisted (npm-like)
Best at
Default / tutorials
Zero-Installs, constraints
Monorepos, disk
Speed + runtime
Install Speed
Numbers below are order-of-magnitude for a mid-size Next.js app (~800–1200 packages) on a warm network. Absolute times vary; relative ranking is stable across public benchmarks.
Scenario
npm
Yarn Berry (PnP)
pnpm
Bun
Cold install (empty cache)
Baseline (slowest)
Fast
Fast
Fastest
Warm cache, clean node_modules
Moderate
Very fast (or Zero-Install)
Very fast (hard links)
Very fast
CI with lockfile + cache hit
Good with npm cache
Excellent / skip install
Excellent
Excellent
Add one dependency
OK
Fast
Fast
Fastest
benchmark-hints.sh
Bash
1
# Rough local timing (same machine, same lockfile generation strategy)
2
time npm ci
3
time yarn install --immutable
4
time pnpm install --frozen-lockfile
5
time bun install --frozen-lockfile
6
7
# Clear only project modules between runs; keep global caches to measure warm path
8
rm -rf node_modules
ℹ
info
Optimize for ci / frozen lockfile installs, not interactive install without a lock. That is the path CI and production images take.
Disk Use
Disk cost has two parts: per-project node_modules (or PnP cache) and the global store/cache shared across projects.
Manager
Per-project footprint
Global store
Notes
npm
Full copies (hoisted tree)
~/.npm cache (tarballs)
Many projects = many copies
Yarn Classic
Similar to npm
Yarn cache
Legacy; prefer Berry or pnpm
Yarn Berry PnP
Zip cache + .pnp.cjs
Optional global cache
Zero-Installs commit cache
pnpm
Symlinks + hard links
Content-addressable store
Best multi-project disk savings
Bun
npm-like tree (optimized)
Bun global cache
Fast; not as thrifty as pnpm store
🔥
pro tip
On a laptop with ten Node monorepos, pnpm's hard-linked store often saves gigabytes versus npm. That is the main reason large orgs standardize on pnpm.
Lockfile Names & Formats
Manager
Lockfile
Human-readable
Commit?
npm
package-lock.json
Yes (JSON)
Always
Yarn
yarn.lock
Yes
Always
pnpm
pnpm-lock.yaml
Yes (YAML)
Always
Bun
bun.lock (text) or bun.lockb (binary)
Text preferred
Always
gitignore-hygiene.sh
Bash
1
# Commit the lockfile that matches your chosen manager — only one
2
# Do NOT commit multiple lockfiles from competing managers
3
4
# Ignore install artifacts
5
echo 'node_modules/' >> .gitignore
6
# Yarn Berry: usually commit .yarn/releases and often .yarn/cache (Zero-Installs)
7
# pnpm: commit pnpm-lock.yaml; do not commit the global store
⚠
warning
Having both package-lock.json and pnpm-lock.yaml in the same repo means different developers may install different trees. Delete the unused lockfile and document the canonical manager in README + packageManager.
node_modules Strategy
How packages appear on disk controls phantom dependencies, peer resolution, and tool compatibility.
Strategy
Used by
Phantom deps
Tooling compatibility
Hoisted flat-ish tree
npm, Yarn Classic, Bun, Yarn node-modules
Easy to accidentally import
Highest
Isolated + symlinks
pnpm (default)
Blocked (good)
High; rare packages need shamefully-hoist
Plug'n'Play
Yarn Berry default
Blocked
Needs SDKs; some packages break
.yarnrc.yml
YAML
1
# Yarn Berry: fall back to classic layout when PnP breaks a tool
2
nodeLinker: node-modules
.npmrc (pnpm)
INI
1
# Escape hatch when a tool expects hoisted packages
2
shamefully-hoist=true
3
# Or hoist only patterns:
4
# public-hoist-pattern[]=*eslint*
5
# public-hoist-pattern[]=*prettier*
Monorepo Support
Feature
npm
Yarn Berry
pnpm
Bun
Workspaces
Yes
Yes (mature)
Yes (mature)
Yes
Filter / foreach
Basic -w
workspaces foreach
--filter (best)
--filter
Catalogs / shared versions
No
Constraints
catalogs
Limited
Strict peers
Warnings
Configurable
Strict by default
npm-like
Typical choice at scale
Small repos
Teams on PnP/Zero-Install
Most monorepos
Speed-first apps
monorepo-commands.sh
Bash
1
# npm
2
npm run build -w @acme/web
3
npm run test --workspaces --if-present
4
5
# Yarn Berry
6
yarn workspaces foreach -A run build
7
yarn workspace @acme/web add zod
8
9
# pnpm
10
pnpm --filter @acme/web build
11
pnpm --filter "./packages/**" test
12
13
# Bun
14
bun run --filter @acme/web build
When to Choose Each
Choose
When
Avoid when
npm
Tutorials, OSS default, zero extra tooling, simple apps
Large monorepos needing strict isolation
Yarn Berry
Want PnP, Zero-Installs, constraints plugins
Team unwilling to adopt PnP SDKs / plugins
pnpm
Monorepos, disk savings, strict deps, catalogs
Tools that hard-require npm flatten (rare; hoist escapes exist)
Bun
Need max install + runtime speed; greenfield
Need maximum Node API parity / conservative ops
✓
best practice
For most new company monorepos in 2025–2026, start with pnpm + Corepack. Use npm for single-package libs that must match contributor defaults. Evaluate Bun when the whole stack (test runner, bundler, runtime) can move together.
Migration Notes
Migrate one repo at a time. Keep CI green on the old manager until the new lockfile is committed and scripts verified.
migrate.sh
Bash
1
# npm → pnpm
2
pnpm import # reads package-lock.json
3
rm package-lock.json
4
pnpm install
5
# set "packageManager": "pnpm@9.x.x" in package.json
6
# enable Corepack in CI
7
8
# npm → Yarn Berry
9
corepack enable
10
yarn set version stable
11
yarn install
12
rm package-lock.json
13
# if PnP breaks tools: yarn config set nodeLinker node-modules
14
15
# npm → Bun
16
bun install # generates bun.lock
17
rm package-lock.json
18
# verify: bun run test && bun run build
19
20
# Any → npm
21
# delete other lockfiles, npm install, commit package-lock.json
Watch for
Why
Fix
Phantom imports
pnpm/PnP stop undeclared requires
Add real dependency or use packageExtensions
postinstall scripts
Security / Bun settings differ
Audit trustedDependencies / ignore-scripts
patched packages
patch-package vs pnpm patch vs yarn patch
Re-apply with native patch tool
engines / only-allow
Block wrong manager
Update preinstall hook after switch
Corepack
Corepack ships with Node and can download/pin Yarn and pnpm versions from the packageManager field. Bun is not managed by Corepack — install Bun separately.
package.json
JSON
1
{
2
"name": "acme",
3
"private": true,
4
"packageManager": "pnpm@9.15.0"
5
}
corepack.sh
Bash
1
corepack enable
2
corepack prepare pnpm@9.15.0 --activate
3
# CI: enable once on the runner image, then pnpm install --frozen-lockfile