Toolmingo
Guides12 min read

npm vs Yarn vs pnpm: Which Package Manager Should You Use in 2025?

A deep-dive comparison of npm, Yarn, and pnpm — covering install speed, disk usage, lockfiles, monorepo support, security, and when to choose each for your JavaScript project.

Every JavaScript project needs a package manager — and for years the choice was trivial (npm). Today you have three serious contenders: npm (the default), Yarn (Facebook's challenger), and pnpm (the disk-space champion). All three install packages from the npm registry, all three manage node_modules, but they make very different trade-offs. This guide tells you exactly what those trade-offs are and when each one wins.

At a glance

npm Yarn (Classic & Berry) pnpm
Creator npm, Inc. (2010) Meta/Facebook (2016) Zoltan Kochan (2017)
Current version v10+ Classic v1 / Berry v4+ v9+
Lockfile package-lock.json yarn.lock pnpm-lock.yaml
Install speed Moderate Fast (Classic) / Fast (Berry) Fastest
Disk usage High (duplicated) High (Classic) / Low (Berry PnP) Very low (hard links)
node_modules structure Flat (hoisted) Flat (hoisted) Non-flat (isolated, symlinks)
Workspaces (monorepo) ✅ (npm 7+) ✅ (first to add) ✅ (best-in-class)
Phantom dependencies ❌ (allows) ❌ (Classic) / ✅ (Berry PnP) ✅ (blocks by default)
Zero-installs ✅ (Berry PnP)
Built into Node.js

Brief history

npm shipped with Node.js 0.6.3 in 2011. For five years it was the only option. The node_modules folder became a running joke — a black hole heavier than the sun — but it worked.

Yarn Classic (v1) launched in 2016. Meta needed reproducible, fast installs across thousands of developers. Yarn introduced the lockfile concept (npm had none at the time), parallel downloads, and an offline cache. It was dramatically faster and the JS community adopted it quickly.

pnpm appeared in 2017 with a radical idea: don't duplicate packages. Instead, keep one content-addressable store on disk and use hard links (and symlinks) into each project. Suddenly node_modules shrank by 50–90%.

Yarn Berry (v2+) launched in 2020 with Plug'n'Play (PnP) — eliminating node_modules entirely, loading packages from a zip cache via a custom Node.js resolver. Powerful but controversial.


How each one stores packages

npm — flat node_modules

npm hoists all dependencies (and their dependencies) into a single flat node_modules/. This means:

my-project/
└── node_modules/
    ├── react/           ← your dep
    ├── loose-envify/    ← react's dep, hoisted up
    ├── js-tokens/       ← loose-envify's dep, hoisted up
    └── ...

Consequence: Your code can require('loose-envify') even though you never declared it — this is the phantom dependency problem.

pnpm — isolated node_modules with a global store

pnpm writes every package version once to a global content-addressable store (~/.pnpm-store). Inside each project it creates a .pnpm folder full of hard links to that store, then symlinks the right entries into node_modules/:

my-project/
└── node_modules/
    ├── react → .pnpm/react@18.3.1/node_modules/react   ← symlink
    └── .pnpm/
        ├── react@18.3.1/
        │   └── node_modules/
        │       ├── react/               ← hard link to global store
        │       └── loose-envify@1.4.0/  ← hard link (react's dep, NOT hoisted)
        └── ...

Consequence: Only packages you explicitly depend on are visible at the top level. Phantom dependencies cause an error.

Yarn Berry PnP — no node_modules at all

Yarn Berry patches Node's module resolution to read packages from .yarn/cache/*.zip files using a .pnp.cjs map. There is no node_modules/ directory.

Consequence: Instant installs after the first one (zero-installs possible by committing the cache), but some tools that assume node_modules/ exist can break.


Speed comparison

Install times for a fresh create-react-app-sized project (cold cache → warm cache):

Scenario npm Yarn Classic pnpm
Cold install (no cache) ~30s ~20s ~15s
Warm install (local cache) ~15s ~8s ~3s
Adding one package ~5s ~3s ~1s
node_modules size (React app) ~340 MB ~300 MB ~120 MB (store shared)

Numbers are approximate benchmarks; your hardware and network matter more than tool differences on cold cache.

pnpm is fastest on warm cache because hard links avoid copying files — it just creates new filesystem entries pointing to already-downloaded data.


Disk usage: the pnpm superpower

If you have 10 projects that all use react@18.3.1:

Tool Disk cost
npm 10 × ~7 MB = 70 MB
Yarn Classic 10 × ~7 MB = 70 MB
pnpm 1 × ~7 MB in store + tiny hard links = ~7 MB

On a machine with dozens of Node.js projects, pnpm can save gigabytes of disk space.


Lockfiles

The lockfile records exact versions (including transitive deps) so that npm install gives every developer identical output.

package-lock.json yarn.lock pnpm-lock.yaml
Format JSON Custom text YAML
Readability Low (huge JSON) Medium High
Merge conflicts Common (large JSON diffs) Moderate Rare (YAML sections)
Committed to git? ✅ Yes ✅ Yes ✅ Yes
Reproducibility

Always commit your lockfile. Never run npm install if your lockfile has been deleted without investigation.


Workspaces (monorepos)

All three support workspaces — the ability to manage multiple packages in one repository.

npm workspaces (package.json):

{
  "workspaces": ["packages/*"]
}
npm install                    # install all packages
npm run build -w packages/ui   # run in specific workspace

Yarn workspaces (identical syntax, added in 2017 — npm copied it in v7):

yarn workspace @myapp/ui add react

pnpm workspaces (pnpm-workspace.yaml):

packages:
  - "packages/*"
  - "apps/*"
pnpm install                    # installs all
pnpm --filter @myapp/ui build   # powerful filtering
pnpm -r test                    # recursive, all workspaces

pnpm's --filter flag is more powerful than npm/Yarn equivalents — it supports dependency graph traversal:

pnpm --filter "...^@myapp/ui" test   # test everything that depends on @myapp/ui

Security

Feature npm Yarn pnpm
Lockfile integrity SHA-512 in lockfile SHA-512 in lockfile SHA-512 in lockfile
Audit command npm audit yarn audit pnpm audit
Phantom dep protection ❌ (Classic)
Script execution on install ✅ (can be blocked) ✅ (can be blocked) ✅ (configurable per package)
Verdaccio (private registry)

The phantom dependency protection in pnpm is a security benefit too: a supply-chain attack can't inject a package into your app via an indirect dependency that you didn't explicitly declare.


Compatibility

Most packages work with all three managers. Edge cases:

Scenario npm Yarn pnpm
Native modules (node-gyp)
Postinstall scripts ✅ (can disable per package)
Tools that read node_modules/ directly ✅ (Classic) / ⚠️ (Berry PnP)
require phantom deps ✅ works (silently) ✅ (Classic) ❌ errors (strictness)
Docker layer caching ✅ (with --frozen-lockfile)
Turborepo ✅ (recommended)
Nx

Yarn Berry PnP has the most compatibility issues — some tools (Jest, native modules, some Babel configs) need extra configuration. The Yarn team maintains a compatibility table.


Common commands cheat sheet

Task npm Yarn pnpm
Install all deps npm install yarn pnpm install
Add package npm install react yarn add react pnpm add react
Add dev dep npm install -D vite yarn add -D vite pnpm add -D vite
Remove package npm uninstall react yarn remove react pnpm remove react
Run script npm run build yarn build pnpm build
Update deps npm update yarn upgrade pnpm update
Interactive update npm install -g npm-check-updates; ncu -u yarn upgrade-interactive pnpm update --interactive
Audit npm audit yarn audit pnpm audit
Global install npm install -g typescript yarn global add typescript pnpm add -g typescript
List installed npm ls yarn list pnpm list
CI/frozen install npm ci yarn install --frozen-lockfile pnpm install --frozen-lockfile
Exec binary npx tsc yarn dlx tsc pnpm dlx tsc
Why is this installed? npm ls react yarn why react pnpm why react

Where npm wins

  • Zero setup — bundled with Node.js. No install needed.
  • Maximum compatibility — every package is tested against npm first.
  • Corporate environments — no "why are you using a non-default tool?" questions.
  • Simplest mental model — flat node_modules, one command to learn (npm install).
  • Most documentation and Stack Overflow answers reference npm.

Where Yarn wins

  • Yarn Berry zero-installs — commit .yarn/cache/ and CI never downloads packages. Great for air-gapped or slow-CI environments.
  • Plug'n'Play strictness — Berry PnP eliminates node_modules entirely, catching phantom deps via the resolver layer.
  • yarn upgrade-interactive — the best interactive dependency upgrade UX of the three.
  • Changesets integration — Yarn Berry + Changesets is the dominant monorepo release workflow.

Where pnpm wins

  • Disk space — non-negotiable winner. Saves GBs on developer machines with many projects.
  • Install speed (warm) — hard links = near-instant reinstall.
  • Phantom dependency protection — strictest without sacrificing node_modules/ compatibility.
  • Monorepo filtering--filter with dependency graph traversal beats npm/Yarn for large monorepos.
  • Turborepo's recommended manager — pnpm is the primary recommendation in Turborepo docs.
  • Strictness without PnP pain — you get most of Yarn Berry PnP's benefits without the compatibility headaches.

Migration paths

npm → pnpm

# Install pnpm
npm install -g pnpm

# In your project root (pnpm reads package.json and generates pnpm-lock.yaml)
pnpm import      # converts package-lock.json → pnpm-lock.yaml
rm package-lock.json
pnpm install

# Tell team members (and CI) to use pnpm
echo "use-pnpm" >> .npmrc   # or set packageManager in package.json

npm → Yarn Classic

npm install -g yarn
rm package-lock.json
yarn install   # generates yarn.lock

npm → Yarn Berry

npm install -g yarn
yarn set version stable   # upgrades to Berry
rm package-lock.json
yarn install   # PnP install, no node_modules/

pnpm → npm (rollback)

rm pnpm-lock.yaml
npm install   # regenerates package-lock.json

Decision guide

Use npm if:

  • You want zero-friction, maximum compatibility
  • Your team doesn't know/care about package managers
  • You're building a library and want CI to mirror what most users experience
  • You're using a managed hosting environment that assumes npm

Use Yarn Classic (v1) if:

  • You're maintaining a project that already uses it (no reason to migrate unless pain)
  • You want upgrade-interactive and slightly faster installs than npm with minimal setup

Use Yarn Berry (v2+) if:

  • You need zero-installs for offline/air-gapped CI
  • You're building a large monorepo with Changesets for releases
  • Your team is experienced and can handle occasional PnP compatibility fixes

Use pnpm if:

  • You have many Node.js projects on one machine (disk savings add up fast)
  • You're starting a new monorepo (best filter, best speed)
  • You want phantom dependency protection without sacrificing node_modules/ compatibility
  • You're using Turborepo (their recommendation)
  • You care about install speed on warm cache

Full comparison table

Feature npm Yarn Classic Yarn Berry pnpm
Built into Node.js
Cold install speed Slow Fast Fast Fast
Warm install speed Medium Fast Fast Fastest
Disk usage High High Low (PnP) Very low
Lockfile readability Low Medium Medium High
Phantom dep protection
Offline cache Partial ✅ (zero-installs)
Workspaces ✅ (v7+) ✅ (best)
node_modules/ present ❌ (PnP) ✅ (symlinked)
Compatibility risk Lowest Low Medium-High Low
Turborepo recommendation
Interactive upgrades via ncu ✅ built-in ✅ built-in ✅ built-in
Global store dedup ✅ (zip cache) ✅ (hard links)
Learning curve Lowest Low Medium Low
Community size Largest Large Medium Growing fast

Common mistakes

Mistake Why it's a problem Fix
Mixing lockfiles (package-lock.json + yarn.lock) Two sources of truth; inconsistent installs Pick one manager, delete the other lockfile, add engines.packageManager
Running npm install in a pnpm repo Generates package-lock.json, installs differently Use only-allow pnpm in package.json scripts
Committing node_modules/ Huge, OS-specific, error-prone Add to .gitignore (Yarn Berry PnP is the one exception)
Not using --frozen-lockfile in CI CI may silently upgrade transitive deps Always use npm ci / yarn install --frozen-lockfile / pnpm install --frozen-lockfile
Ignoring peerDependencies warnings Runtime type mismatches, multiple React instances Read the warnings; install missing peer deps
Using npm install -g for project tools Version mismatch across machines Add to devDependencies, use npx / pnpm dlx
Phantom dependencies (hoisted packages) Works locally, breaks when dep tree changes Use pnpm or explicitly declare all deps
yarn upgrade --latest without review Breaks package.json version ranges Use upgrade-interactive and review each bump

FAQ

Which is fastest? pnpm on warm cache (packages already in global store). The difference is negligible on cold CI installs where network is the bottleneck.

Should I migrate from npm to pnpm in an existing project? Yes, if you have a monorepo or many projects on your machine. Run pnpm import to convert the lockfile — it takes under a minute. The only risk is phantom dependencies surfacing as errors; fix them by explicitly adding those packages.

Is Yarn Berry stable? Yes, but check the compatibility table for your tools. Jest, Vite, and most modern tools are supported. Some native-module packages need nodeLinker: node-modules in .yarnrc.yml to fall back to classic node_modules/.

Does pnpm work with Docker? Yes. Use the official pnpm Docker pattern:

FROM node:20-alpine
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

What is Corepack? Corepack is a Node.js (v16.9+) tool that lets you pin the package manager version per project:

{
  "packageManager": "pnpm@9.1.0"
}

Running pnpm install in this project automatically downloads the pinned pnpm version. It works for npm, Yarn, and pnpm.

Which does Vite/Turborepo/Next.js recommend? Vite: no preference. Turborepo: pnpm (primary examples). Next.js: no preference (shows npm and pnpm examples). Nx: supports all three.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools