Skip to content

Latest commit

 

History

248 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Benchmarks of JavaScript Package Managers

The current numbers are in benchmarks.json, and are published as a page at pnpm.io/benchmarks.

This repository measures; it does not present. It writes one file — benchmarks.json — carrying the numbers, the version of every tool they were measured with, and the conditions they were measured under. Which of them a reader is shown, what the columns and rows are called, how the charts look and what the prose around them says are all decided in pnpm/pnpm.io, which reads that file and renders the page from it (scripts/update-benchmarks.mjs there). Wording and presentation change far more often than the measurement does, and keeping them apart means neither drags the other through a benchmark run.

Usage

pnpm install
pnpm run benchmark

A run measures every package manager, appends the timings to results/<manager>/<version>/<fixture>.yaml, and rewrites benchmarks.json from them. The published number for a tool at a version is the minimum of the samples recorded for it — the statistic lives here rather than downstream, because only the code that took the samples knows what they are samples of.

pnpm run report rewrites benchmarks.json from results already recorded, without provisioning a package manager or starting a registry. It measures nothing, so it cannot invent a number a measuring run failed to record; it fails instead.

The benchmark installs every measured package manager itself, with pnpm 12: it bootstraps a pnpm@next-12 and uses pnpm add --global / pnpm self-update to provision npm, pnpm, Yarn, and Bun — each into an isolated directory of its own (setupPackageManagers.js). That covers Yarn 6, which is not on the registry anymore — it ships as a platform binary that pnpm fetches from Yarn's GitHub releases. GitHub rate-limits anonymous API calls, so on CI a GITHUB_TOKEN should be exported for the release-list request.

Every manager listed in commandsMap.js is measured and reported. Which of them a reader is shown is a separate question, answered downstream: pnpm.io draws npm and the pnpm columns, and the rest are measured for comparisons of our own.

The registry the install benchmark runs against

Every package manager installs through a pnpr registry of the benchmark's own rather than npmjs, and pnpm 12 is measured a second time with dependency resolution offloaded to that server. benchmarkRegistry.js brings all of that up. pnpr is installed from the registry by the benchmark, like the package managers are, and needs no setup.

Four things about it are worth knowing before changing it.

The registry is reached across an emulated network. A registry on the benchmark machine hides what resolving a dependency graph costs — round trips — which is exactly what the pnpm + pnpr column exists to measure.

The accelerated scenario makes the server resolve on loopback. pnpr resolves against the registry URL the client sends, and the client's configured registry sits behind the emulated link — so left alone, the server would fetch every packument by crossing that link back into its own public endpoint, paying per graph level the very round trips server-side resolution exists to remove. The accelerated scenario therefore tells the client (through the PACQUET_BENCHMARK_* env hooks pnpm carries for the pnpm monorepo's integrated-benchmark task, which models the same thing) to have the server resolve against its own un-proxied address, declared as a route in the server's config. That is the production shape — a resolver co-located with its registry — while tarballs still cross the emulated link like every other scenario's.

The numbers are recorded under their own fixture name (alotta-files-pnpr), and the runs recorded before the benchmark installed through pnpr are left where they are under alotta-files. They were measured against a different registry over a different network, so pooling the two would average unrelated things together. The same rule holds within the name: change the setup in a way that changes what a number means, and the runs recorded before the change have to be deleted, or min() pools them with the new ones and a number from the old setup stands in for the new one.

The network is emulated by latencyProxy.js, which every client's traffic passes through — including pnpm's resolution requests, at the same round trip, so no client gets a cheaper link than another. The bandwidth cap is enforced for the link as a whole, not per connection: package managers differ enormously in how many connections they open, and a per-connection cap silently hands the most connection-hungry client a multiple of the configured bandwidth — enough to finish a download-bound install in less time than the bytes could physically cross the configured wire. It runs as a process of its own because installs are measured with a synchronous spawn: a proxy inside the benchmark process would accept no connection until the install it is serving had already finished. The same reasoning applies to output — pnpr and the proxy write to files rather than pipes, because nothing drains a pipe while a measured install holds the event loop, and a full pipe buffer would stop the server answering mid-scenario.

Node.js version management

The Node.js version management section compares pnpm with fnm and nvm. nvm is cloned by the benchmark itself, but fnm has to be on PATH:

curl -fsSL https://fnm.vercel.app/install | bash

That section times commands inside the shell with $EPOCHREALTIME, so it needs Bash 5 or newer on PATH and refuses to measure without it. macOS still ships Bash 3.2 as /bin/bash:

brew install bash

About

Benchmarks of JavaScript Package Managers

Topics

Resources

Stars

433 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages