メインコンテンツまでスキップ

Benchmarks of JavaScript Package Managers

Last benchmarked at: Aug 28, 2026, 2:16 PM (weekly updated).

This benchmark compares the performance of npm and pnpm. Every package manager installs through the same pnpr registry (v0.1.0-alpha.7) across an emulated 50ms round trip at 200 Mbit/s, so they all face one registry over one reproducible network instead of whatever link the benchmark machine happens to have. pnpm 12 is measured twice: once on its own, and once resolving its dependency graph on the server instead of walking it itself. The page also compares how fast pnpm, fnm, and nvm install and switch Node.js versions.

About the setup:

  • Every manager crosses the same link. The round trip is applied to all of them, and to pnpm's resolution requests as well, so no client gets a cheaper connection than another. The bandwidth cap is the link's, shared across all of a manager's connections — opening more connections in parallel spreads the latency, as on a real network, but cannot multiply the 200 Mbit/s.
  • pnpr's cache is warmed before anything is timed — with the fixture's dependency graph and with the one the update row installs — so no manager pays to pull either into the registry on behalf of the ones measured after it.
  • Server-side resolution pays off when there is a graph to resolve. Resolving one means walking it level by level, and each level costs a round trip, so the cost is roughly the depth of the graph times the latency. pnpr does that walk next to the registry — its own metadata access stays on loopback, the co-located shape the pnpm monorepo's integrated benchmark measures — and answers with the whole resolved lockfile at once, which is why the rows without a lockfile, and the row that changes dependencies, are the ones where it pulls ahead of plain pnpm.
  • The lockfile is trusted, so both managers are asked for the same work. pnpm verifies a lockfile against the registry before installing it — a supply-chain pass that costs a packument per package, and one npm doesn't perform. The rows with a lockfile run every pnpm column with trustLockfile, so what they compare is the install rather than a safety check only one participant was asked for. It is on by default outside this benchmark, and pnpm's own resolution still applies its release-age policy on the rows that resolve.
  • With an up-to-date lockfile there is nothing to resolve. pnpm doesn't ask the server then, so those rows measure the same install in both pnpm 12 columns.
  • The update row starts from a warm cache. The rows before it delete the cache twice, and how much of it a manager has rebuilt by the time update runs is an accident of row ordering — restoring a warm node_modules without touching the registry leaves the cache cold, while re-downloading the whole graph on the same row refills it. A developer who bumps versions has the cache their installs left, so before the update is timed, every manager re-fetches the base graph once, untimed.
  • Tarballs are still fetched by the client, in parallel and directly, on every row.

Each row's label lists which of cache, trusted lockfile, and node_modules are warm/present before install runs. Quick mapping to the real world (ordered from slowest to fastest scenario):

  • clean: a brand-new clone — nothing cached, no lockfile, no node_modules.
  • trusted lockfile: a CI server doing its first install.
  • cache: a developer reinstalling without a lockfile.
  • cache+trusted lockfile: a developer reinstalling a known project.
  • cache+node_modules: the lockfile is deleted and install is run again.
  • node_modules: the cache and lockfile are deleted and install is run again.
  • cache+trusted lockfile+node_modules: re-running install when nothing has changed.
  • trusted lockfile+node_modules: the cache is deleted and install is run again.
  • update: dependency versions are bumped in package.json and install is run again, from a warm cache.

Lots of Files

The app's alotta-files package.json

actioncachetrusted lockfilenode_modulesnpmpnpmpnpm 🦀pnpm + pnpr
install45.9s8.2s5s3.3s
install11.5s4.7s3.2s3.2s
install10.9s4s964ms741ms
install7.2s2.1s635ms682ms
install1.4s598ms48ms53ms
install1.4s580ms67ms71ms
install1s472ms15ms17ms
install1s610ms63ms67ms
updaten/an/an/a3.8s3.5s949ms1s
Graph of the alotta-files results

pnpm vs pnpm 🦀

pnpm v12 will use a new installation engine for fetching and linking written in Rust. See pacquet.

actioncachetrusted lockfilenode_modulespnpmpnpm 🦀
install8.2s5s
install4.7s3.2s
install4s964ms
install2.1s635ms
install610ms63ms
install598ms48ms
install580ms67ms
install472ms15ms
updaten/an/an/a3.5s949ms
Graph comparing pnpm versions on the alotta-files fixture

Node.js Version Management

pnpm installs and switches Node.js versions itself, so a separate version manager is not needed. This section compares pnpm runtime set node with fnm and nvm.

scenariopnpm 12fnmnvm
install Node.js 24 with nothing cached1s2.3s2.7s
install Node.js 24 that was installed before146ms2.4s2.3s
run node in a project pinned to Node.js 228ms3ms81ms
Graph comparing Node.js version managers on installing Node.js

A few things to keep in mind when reading these numbers:

  • pnpm keeps Node.js in its content-addressable store and nvm keeps the downloaded tarballs in $NVM_DIR/.cache, so for both of them installing a version that was installed before needs no download. fnm has no download cache and fetches Node.js again.
  • pnpm doesn't extract the npm, npx, and corepack binaries bundled with Node.js, so on a clean install it downloads and writes fewer files than the other two.
  • Per-project switching costs no command at all in pnpm: the node on your PATH is a shim that reads the devEngines.runtime of the project and runs the matching version. fnm and nvm read a .node-version or .nvmrc file through a shell hook that fires on cd, which is what fnm exec and nvm use measure in that row. Loading nvm into the shell in the first place is not counted at all here, and it costs more than everything in that row.
  • All three have to materialize the pinned version the first time a project asks for it: pnpm links it from its store, fnm downloads it, nvm unpacks it. The row measures the repeated runs after that.