Every JavaScript team hits the same fork in the road: npm vs Yarn vs pnpm. Most comparison articles repeat the same claims (“pnpm is faster”, “Yarn has better workspaces”) without showing the numbers or telling you what actually changes in your daily workflow.
So we did the boring part. We took one React front end and one Node API, ran the three package managers on identical hardware, measured cold installs, warm installs, CI installs and disk footprint, then compared what each lockfile does when a real team touches it. At the end you get a decision table (monorepo, CI pipeline, solo project) and copy-paste migration steps in every direction.
TL;DR: the short answer
- pnpm won our benchmark on the metric that matters most day to day: repeat installs and disk usage. It used roughly 2.3x less disk than npm on the same React project and installed about 2.5x faster from a warm cache.
- Yarn 4 with Plug’n’Play was the fastest of all in CI, but it is the biggest behavioral change and still trips up tooling that expects a real
node_modulesfolder. - npm is the slowest and the hungriest on disk, but it is the only one that is already on every machine, in every Docker image and in every tutorial. For a small solo project, that is a perfectly rational reason to stay.
If you want one sentence: pnpm for monorepos and CI, npm for simplicity and zero setup, Yarn when you need PnP performance or you are already on Yarn Berry and it works. betterstack.com has covered this at length.

Benchmark setup
Numbers without context are marketing, so here is exactly what we ran.
| Machine | 8 vCPU, 16 GB RAM, NVMe SSD, Ubuntu 24.04 |
| Runtime | Node.js 24 LTS |
| Package managers | npm 11.x, Yarn 4.x (node-modules linker and PnP), pnpm 10.x |
| Project A | React 19 + Vite + TypeScript + Tailwind + React Router + TanStack Query + Vitest (~1,150 packages resolved) |
| Project B | Node API: Fastify + Prisma + Zod + Pino + Jest (~780 packages resolved) |
| Runs | 5 per scenario, median reported, network on a stable 500 Mbps link |
Three scenarios were measured, because they are not the same problem:
- Cold install: no lockfile, no local cache, no
node_modules. This is the “first day on the project” case and the worst case in a fresh container. - Warm install: lockfile present, global cache/store populated,
node_modulesdeleted. This is what a developer feels after switching branches. - CI install: lockfile present, frozen lockfile flag on, cache restored from the CI cache action. This is the number that multiplies by every pipeline run you pay for.

Install speed results
Project A: React 19 + Vite front end
| Package manager | Cold install | Warm install | CI (frozen lockfile) |
|---|---|---|---|
| npm 11 | 47.9 s | 23.6 s | 30.8 s (npm ci) |
| Yarn 4 (node-modules) | 40.7 s | 15.9 s | 20.6 s |
| Yarn 4 (PnP) | 33.1 s | 4.9 s | 6.3 s |
| pnpm 10 | 32.4 s | 9.2 s | 11.8 s |
Project B: Fastify + Prisma API
| Package manager | Cold install | Warm install | CI (frozen lockfile) |
|---|---|---|---|
| npm 11 | 39.2 s | 19.4 s | 25.1 s |
| Yarn 4 (node-modules) | 33.5 s | 13.7 s | 17.2 s |
| Yarn 4 (PnP) | 28.9 s | 5.4 s | 7.1 s |
| pnpm 10 | 27.6 s | 8.1 s | 10.4 s |
What the numbers actually mean
- Cold installs are network bound. The gap between the three shrinks to 15 to 30 percent because everyone is waiting on the registry. Do not choose a package manager based on cold install alone.
- Warm installs are filesystem bound. This is where pnpm’s content-addressable store and hard links crush npm’s copy-everything approach, and where Yarn PnP wins outright because it barely writes files at all.
- CI is where money is. Moving a 20-developer team from
npm ciat 31 s to pnpm at 12 s saves roughly 19 s per pipeline run. At 300 runs per week that is about 1.6 hours of runner time saved every week, per repository.
One caveat we hit and you will too: pnpm 10 blocks lifecycle scripts of dependencies by default. Packages like Prisma or esbuild that need a postinstall step must be allowed explicitly via onlyBuiltDependencies in package.json or pnpm-workspace.yaml. It is a two-minute fix and a genuine security win, but it will surprise you the first time a binary is missing.
Disk usage: the difference nobody notices until the SSD is full
Same projects, freshly installed, measured with du -sh.
| Package manager | React project on disk | Node API on disk | Both projects together |
|---|---|---|---|
| npm 11 | 412 MB | 308 MB | 720 MB |
| Yarn 4 (node-modules) | 396 MB | 296 MB | 692 MB |
| Yarn 4 (PnP) | 131 MB (zipped cache, no node_modules) | 98 MB | 229 MB |
| pnpm 10 | 178 MB (store + links) | 142 MB | 221 MB (shared store) |
The interesting column is the last one. npm and Yarn Classic-style installs duplicate every shared dependency in every project. pnpm stores each package version once per machine and hard links it into each project, so the marginal cost of your fifth project using React 19 is close to zero.
We pushed it further with five copies of the React project on the same machine:
- npm: 2.01 GB
- Yarn 4 node-modules: 1.93 GB
- pnpm: 412 MB total (one store, five link trees)
If you keep several branches checked out as worktrees, or you are the person on the team who clones every client repo, this single difference is worth the migration.

Lockfile differences that actually affect your team
Format and readability
| Feature | npm | Yarn 4 | pnpm 10 |
|---|---|---|---|
| File | package-lock.json (v3) |
yarn.lock (YAML) |
pnpm-lock.yaml (v9) |
| Size on our React project | ~695 KB | ~412 KB | ~330 KB |
| Human readable diffs | Noisy JSON, large diffs | Good | Good, importers separated from packages |
| Merge conflicts | Frequent and painful | Auto-resolves many conflicts on install | Rare, conflicts are localized |
| Frozen install command | npm ci |
yarn install --immutable |
pnpm install --frozen-lockfile |
| Integrity hashes | Yes (sha512) | Yes (checksum field) | Yes (integrity per resolution) |
| Overrides / patching | overrides |
resolutions + native yarn patch |
pnpm.overrides + pnpm patch |
Behavior differences worth knowing
- npm rewrites the lockfile more eagerly. Running
npm installafter someone else changedpackage.jsoncan reshuffle a large part of the tree, producing 2,000-line diffs that nobody reviews.npm ciis the only way to guarantee the lockfile wins. - Yarn’s
--immutablefails loudly if the lockfile would change, and it is the default in CI when Yarn detects a CI environment. This is excellent for reproducibility. - pnpm’s lockfile separates “importers” (your workspace packages) from resolved packages, so in a monorepo you can see at a glance which package changed. That alone makes code review of dependency bumps realistic.
- Strictness of the tree: npm and Yarn node-modules hoist dependencies, so your code can import a package you never declared (phantom dependency). pnpm’s symlinked layout and Yarn PnP both block that. It breaks a few sloppy packages on migration day and then saves you from a production surprise later.
- Supply chain controls: after the npm ecosystem incidents of the past year, delayed-adoption settings matter. pnpm supports a minimum release age setting so freshly published versions are not installed immediately, and it blocks dependency lifecycle scripts by default. Yarn PnP’s zipped, checksummed cache is also easy to audit. npm relies more on you configuring policies yourself.
Decision table: which package manager fits your situation
| Your context | Best choice | Why |
|---|---|---|
| Monorepo, 5 to 100 packages | pnpm | Workspaces plus catalogs, strict deps, shared store, --filter is fast and predictable |
| Monorepo already on Yarn Berry | Stay on Yarn 4 | Constraints, plugins and PnP are strong; migration cost rarely pays off if it works |
| CI pipeline cost is a problem | pnpm, or Yarn PnP if tooling allows | 2 to 5x faster restore, smaller cache artifacts |
| Solo project or prototype | npm | Zero install, zero config, every doc and AI assistant assumes it |
| Open source library with outside contributors | npm or pnpm | Lowest friction for drive-by pull requests; pnpm is one Corepack line away |
| Docker images and serverless bundles | pnpm (with --node-linker=hoisted if needed) |
Smaller layers; watch out, hard links do not cross Docker layers, use a mounted store |
| Legacy app, old build tooling, React Native | npm or Yarn node-modules | Some native tooling still assumes a flat node_modules |
| Strict compliance, private registry, audit trail | pnpm or Yarn | Blocked postinstall scripts, minimum release age, immutable installs |
| Disk space on laptops is tight | pnpm | One store per machine instead of one copy per project |
Scoring summary
| Criterion | npm | Yarn 4 | pnpm 10 |
|---|---|---|---|
| Speed (warm / CI) | 2/5 | 4/5 (5/5 with PnP) | 5/5 |
| Disk efficiency | 1/5 | 3/5 (5/5 with PnP) | 5/5 |
| Monorepo support | 3/5 | 5/5 | 5/5 |
| Ecosystem compatibility | 5/5 | 3/5 with PnP, 5/5 with node-modules | 4/5 |
| Learning curve | 5/5 | 3/5 | 4/5 |
| Supply chain defaults | 3/5 | 4/5 | 5/5 |

Migration steps between package managers
Pin the tool for everyone first. Add this to package.json so Corepack (or your CI setup step) uses the same version on every machine:
"packageManager": "pnpm@10.x.x"
From npm to pnpm (the most common move)
- Install pnpm:
corepack enable pnpmornpm i -g pnpm. - Import the existing lockfile so versions do not drift:
pnpm import(readspackage-lock.jsonand writespnpm-lock.yaml). - Delete the old artifacts:
rm -rf node_modules package-lock.json. - Install:
pnpm install. - Allow the postinstall scripts you actually need, for example Prisma, esbuild, sharp, husky, via
onlyBuiltDependencies. Runpnpm approve-buildsto see what was blocked. - Fix phantom dependencies. If something fails with “cannot find module X”, add X to your
package.json. That is the point of the strict tree. - Update scripts and CI:
npm cibecomespnpm install --frozen-lockfile,npm run buildbecomespnpm build. - Add
.npmrcsettings if you need looser hoisting:shamefully-hoist=trueis the escape hatch, use it only as a temporary bridge. - Commit
pnpm-lock.yaml, and addpackage-lock.jsonandyarn.lockto.gitignoreto avoid mixed states.
From Yarn to pnpm
pnpm importalso readsyarn.lock(Yarn Classic and Berry node-modules mode).- Translate
resolutionsinpackage.jsonintopnpm.overrides. - Convert workspace globs: Yarn uses
workspacesinpackage.json, pnpm usespackages:inpnpm-workspace.yaml. - Replace
yarn workspace app buildwithpnpm --filter app build. - Re-apply patches:
yarn patchoutput must be regenerated withpnpm patch. - Remove
.yarnrc.yml,.yarn/and.pnp.cjsonce everything is green.
From pnpm or Yarn back to npm
- Delete
node_modulesand the foreign lockfile. - Run
npm installand review the resultingpackage-lock.jsoncarefully, versions may float within your semver ranges. - Move
pnpm.overridesorresolutionsinto npmoverrides. - Convert workspace filters:
npm run build --workspace=app. - Run your full test suite. This direction is the riskiest because hoisting changes which version of a transitive dependency your code resolves.
From npm or pnpm to Yarn 4
corepack enablethenyarn set version stable.- Run
yarn importif you are coming frompackage-lock.json, otherwiseyarn install. - Choose your linker in
.yarnrc.yml: start withnodeLinker: node-modules, move to PnP only once your tooling is verified. - If you go PnP, run
yarn dlx @yarnpkg/sdks vscodeso your editor resolves types correctly. - Set
enableGlobalCache: trueto avoid committing the cache, or use zero-installs deliberately if that is your strategy.
Migration rules that save weekends
- Never mix lockfiles in the same repository. One package manager, one lockfile, enforced by a preinstall guard such as
only-allow. - Migrate on a quiet branch, run the full CI matrix, and merge early in the week.
- Compare production bundles before and after, not just “it builds”. A different resolution of a transitive dependency can change output size or behavior.
- Update your Dockerfile cache mounts. For pnpm use a mounted store, for Yarn use the global cache folder, otherwise your image build gets slower, not faster.
What about Bun?
Bun’s installer is genuinely fast and it will beat all three on raw numbers in many cases. We left it out of the headline comparison because the question this article answers is about the three package managers most teams already have in production. If you are evaluating Bun as a runtime and package manager together, benchmark it on your own repo, and pay close attention to native modules, workspace edge cases and how your CI provider caches it.

Our recommendation
- New project, team of two or more, or any monorepo: start with pnpm. Best speed-to-effort ratio, best disk behavior, strictest defaults.
- Existing Yarn Berry project that works: keep Yarn 4, and consider PnP if your toolchain supports it. The CI numbers are hard to beat.
- Solo project, tutorial, throwaway prototype, or a repo maintained by people who do not live in JavaScript: npm is fine. Use
npm ciin CI and move on.
The worst outcome is not choosing “the wrong one”. It is having three lockfiles in the same repository and a CI job that installs with a different tool than your developers. Pick one, pin it with the packageManager field, and enforce it.
FAQ
Which is better, npm, Yarn or pnpm?
For most modern teams, pnpm is the best overall choice: it was 2 to 2.5x faster than npm on warm and CI installs in our benchmark and used about 2.3x less disk on a single project, far less across multiple projects. npm is better when you value zero setup and maximum compatibility. Yarn 4 is best if you are already on it or you need Plug’n’Play performance. Full details on https://codemancers.com.
Is Yarn still better than npm?
Yarn 4 is still faster than npm in every scenario we measured, and its immutable installs and workspace tooling are more mature. However npm has closed a lot of the historical gap, so “Yarn because npm is slow” is a weaker argument than it was five years ago. The bigger differentiator today is Yarn PnP, not Yarn itself.
Is pnpm still faster than npm?
Yes. In our tests pnpm finished a CI install of a React 19 project in 11.8 s versus 30.8 s for npm ci, and a warm install in 9.2 s versus 23.6 s. The advantage grows the more projects and branches you keep on the same machine, because pnpm reuses one global content-addressable store. You can see it done properly by a design studio worth a look.
Are pnpm and npm the same?
No. They read the same package.json and the same registry, but they build node_modules completely differently. npm creates a flat, hoisted, duplicated tree. pnpm creates a symlinked tree that points into a shared store, which is why it is smaller, faster and stricter about undeclared dependencies.
Can I use pnpm and npm in the same project?
You should not. Two lockfiles will drift and produce different dependency trees for different people. Pin one manager with the packageManager field in package.json and block the others with a preinstall check.
Does switching package managers break my app?
It can, in two predictable ways: phantom dependencies (code importing packages that were only available through hoisting) and postinstall scripts that pnpm blocks by default. Both are quick to fix, but always migrate on a branch with the full test suite running.
Which package manager is best for a monorepo?
pnpm and Yarn 4. Both have mature workspace support, filtered commands and dependency constraints. npm workspaces work, but they are more basic and slower once you pass a handful of packages.
Which is best for CI pipelines?
pnpm with --frozen-lockfile and a cached store gives the best speed-to-compatibility ratio. Yarn PnP is faster still if your tooling supports it. Whatever you pick, always use the frozen or immutable install flag so CI can never silently change your lockfile.

