Years ago npm cost me too much time and disk, so I moved to Yarn and stopped thinking about it. Then I started a new project, and that old decision was suddenly five years stale. So I re-ran the comparison: pnpm 12.8.1, npm 12.2.0, Yarn 4.18.1 (two linkers), Yarn Classic 1.22.22 and Bun 1.4.2, all against the same lockfile-pinned React 19 + Vite app, plus a 12-workspace monorepo.
How Was It Measured?
Everything ran on one Linux box (Xeon 2.1 GHz, 4 cores, 15 GB, ext4) with Node 22.23.3, each manager installed into its own isolated prefix with its own cache. Five runs per cell on the app, three on the monorepo, median shown with the range in brackets. Cold order was shuffled between repetitions so a network hiccup hit everyone. The harness, the fixtures and every raw run are in the repo under audits/coding-dunia/2026-10-03-package-manager-benchmark/.
The caveat that matters: the sandbox reached the npm registry in about 0.08s per tarball, which is much faster than a typical CI runner. Cold numbers here are therefore smaller than yours, and cold differences are mostly noise. Warm numbers, where nothing touches the network, are the trustworthy ones.
| Seconds, single app | npm 12.2 | Yarn 1 | Yarn 4 nm | Yarn 4 PnP | pnpm 12.8 | Bun 1.4 |
|---|---|---|---|---|---|---|
Cold, lockfile (frozen-cold) | 4.45 [4.34-5.98] | 14.6 [13.9-15.4] | 5.13 [5.00-6.13] | 3.77 [3.72-4.45] | 2.20 [2.17-4.20] | 3.48 [3.33-5.16] |
Warm cache (frozen-warm) | 3.39 [3.07-3.53] | 3.28 [2.21-3.40] | 2.71 [2.62-3.00] | 1.37 [1.24-1.70] | 0.25 [0.14-0.41] | 0.086 [0.077-0.12] |
| Nothing changed | 0.54 | 0.28 | 1.00 | 0.96 | 0.036 | 0.011 |
| Add one package | 0.62 | 1.70 | 1.33 | 1.29 | 0.23 | 0.052 |
Read the cold row honestly. pnpm, Bun and Yarn PnP overlap, so call that a tie. npm sits clearly behind pnpm, Yarn 1 is clearly last. The monorepo told the same story: warm install 4.68s for npm, 0.39s for pnpm, 0.12s for Bun.
How Much Disk Do They Use?
One project barely separates them. Three projects sharing one cache separate them a lot, and that's the number the old benchmark of this article couldn't show.
MiB on disk (one du, hardlinks counted once) | npm | Yarn 1 | Yarn 4 nm | Yarn 4 PnP | pnpm | Bun |
|---|---|---|---|---|---|---|
| One project | 217 | 1,084 | 547 | 424 | 176 | 221 |
| Three projects, same cache | 607 | 1,583 | 969 | 562 | 232 | 277 |
pnpm adds about 28 MiB per extra project because it links from one store. npm adds about 195. Bun does nearly as well as pnpm once the cache is shared, which surprised me. Yarn Classic keeps extracted copies of everything, and its cache alone was 888 MB. One warning on method: du counts hardlinked files once per invocation, so summing two separate du calls double-counts and made pnpm look twice as big (349 MiB) until I measured project and store together.
What Actually Broke?
Speed is the number every landing page quotes. Breakage is the one that costs a day. Four things surfaced.
Yarn PnP failed my test run. vitest run with jsdom died with Failed to start forks worker, root cause ERR_VM_MODULE_LINK_FAILURE inside html-encoding-sniffer. Typecheck and build passed. I found no workaround, and Vite itself prints that PnP is discouraged.
Phantom dependencies split the field. I imported picocolors, which no package.json declares but a dependency pulls in. npm, Yarn 1, Yarn 4 node_modules and Bun resolved it happily. pnpm and Yarn PnP refused. Refusing is the right behavior, because that import works on your machine and dies the day the parent dependency drops it.
Three managers blocked or changed my install by default. Yarn 4.18.1 refused to install a pinned @tanstack/react-query that had been published the previous day, because of its release-age gate. pnpm 12.8.1 has a similar gate and wrote its own exclusions into pnpm-workspace.yaml. On the monorepo, pnpm also failed with ERR_PNPM_IGNORED_BUILDS until I allowed esbuild's install script. Annoying for ten minutes, and defensible: both defaults exist to blunt supply-chain attacks.
npm refused a conflict the others waved through. latest TypeScript was 7.0.2, and typescript-eslint 8.71.0 wants <6.1.0. npm stopped with ERESOLVE. The other four installed with warnings. I'd call npm correct here.
The fastest package manager is the one whose defaults will not surprise you in year three, and a speed gap you can only measure warm is not worth trading a decade of track record for.
Which Tool Has the Track Record?
You asked yourself the right question if you asked what happens in two years. Speed on day one is cheap to measure. Survival isn't. The registry gives hard dates for the first stable release:
| Tool | First 1.0 / 4.0 release | Versions published | Note |
|---|---|---|---|
| npm | ships with Node.js | 609 | the default everywhere |
| Yarn Classic | 1.0.0, Sep 2017 | 120 | maintenance mode |
| pnpm | 1.0.0, Jun 2017 | 1,336 | major 12 now |
| Yarn 4 | 4.0.0, Oct 2023 | 100 | PnP discouraged by Vite |
| Bun | 1.0.0, Sep 2023 | 1,343 | runtime plus package manager |
pnpm and Yarn Classic are the same age, yet only one of them kept shipping. That, plus a store design that has not changed in nine years, is why I'm comfortable calling pnpm the pick. Bun's release cadence is impressive and its age is three years. If its install layout stays as inconsistent as I saw between single apps and workspaces, it's the one I'd re-check first. For what it's worth, the weekly download count of pnpm's own package was 236 million in the week to 1 October, against 11 million for Yarn Classic and 4.6 million for Bun, but CI runners inflate that number, so treat it as a direction, not a headcount.
What Is My Rule for Picking Tools?
I don't chase the new thing. A tool that looks better today often turns into a maintenance problem a year or two later, and then the project it was supposed to help quietly dies with it. So before I switch anything I ask one question: does this tool have a long enough history and wide enough use that it will still be fine in several years? If the answer is no, I'd rather stay on something imperfect that most people have used for a long time than jump to a newcomer and jump back. That rule is why Bun, for all its speed, isn't my pick, and why pnpm is: it passed the benchmark and the age test.
So Which One Should You Pick?
| If you value | Pick |
|---|---|
| Speed, disk and strictness together, long track record | pnpm |
| Zero extra tooling, smallest surprise | npm |
| Raw warm speed, willing to retest in a year | Bun |
| An already-working Yarn 4 setup with node_modules linker | Stay, no urgency |
| Yarn Classic or Yarn PnP | Plan a move |
My decision: pnpm. I moved the Yarn 4 monorepo behind this site to it, and the move is merged. Cold install went from 104.9s to 8.8s and disk from about 3.0 GiB to 1.65 GiB on my Linux box, with identical test and typecheck results. The migration guide covers what broke on the way. New projects get pnpm from day one. I'll update this article once the move has run on CI for a few weeks, because a benchmark is a prediction and a migrated repo is the evidence.
Set packageManager in package.json so everyone gets the same version. Pick pnpm for the dependency rules rather than the stopwatch. If a switch later turns out to be a mistake, the exit is cheap as long as the old lockfile stays in git history: in my tests a revert restored it byte for byte, while going back without it moved about 75 of 261 package versions. The step-by-step version of that move, including the rollback, is in the same guide.
One honest limit: these are one fixture on one machine on one day, so rerun the harness on yours. And the reasoning behind weighing exit cost over benchmark wins is in the exit cost criterion. I'll rerun this when pnpm 13 or Yarn 5 ships.