Plate 79
pm-detector vs which-pm: 0.49 ms vs 0.043 ms
Aditya Challa6 min read
I timed package-manager-detector 1.9.0 detect against which-pm 4.0.0 on a package-lock fixture on this box: 0.49 ms vs 0.043 ms per call. which-pm returned null on that lockfile-only tree; package-manager-detector reported npm. Gzip for the bundled detect+agent+resolveCommand entry was 2,501 B versus which-pm's 18,635 B.
Short answer: pick package-manager-detector when you need lockfile / packageManager-field detection, agent metadata, and resolveCommand helpers. Pick which-pm when you only care what already installed into node_modules (or bun.lockb) and want a tiny sync-ish check. They answer different questions. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 03:55 IST.
- Versions: package-manager-detector 1.9.0, which-pm 4.0.0, which-pm-runs 2.0.0 (npm latest that day), Node 20.19.2, esbuild 0.28.2 node ESM minify + gzip -9.
- Workload: seven fixtures under /workspace/bench-pm-detector-which-pm/fixtures/ (npm-lock, pnpm-lock, yarn-lock, empty, nm-only, with-pm-field, bun-lock). Timed detect({ cwd, stopDir }) vs whichPM(cwd), plus walking detect without stopDir, plus getUserAgent vs whichPMRuns with a fake npm_config_user_agent. Median of 9 × 200 async calls, two processes. Scripts and JSON in /workspace/bench-pm-detector-which-pm/.
- Also measured: esbuild bundles (full pmd, detect-only, which-pm, which-pm-runs) and alone-on-disk install size.
- Not tested: yarn Berry integrity files beyond .yarn-integrity, rush.json edge cases, Windows path walks, Bun/Deno hosts, and Node 22+ even though which-pm / which-pm-runs declare engines.node >= 22.13 (both still ran here on 20.19.2).
How fast is package-manager-detector vs which-pm?
| Job (median of 9, ms, stopDir = fixture) | pmd detect | which-pm |
|---|---|---|
| package-lock.json present | 0.493 | 0.043 |
| pnpm-lock.yaml present | 0.407 | 0.045 |
| yarn.lock present | 0.466 | 0.041 |
| bun.lock + empty bun.lockb | 0.236 | 0.043 |
| empty package.json only | 0.645 | 0.045 |
| node_modules dir, no lock | 0.652 | 0.044 |
| packageManager field pnpm@9.12.0 | 0.554 | 0.042 |
which-pm was about 11× faster on the package-lock fixture (0.043 vs 0.49 ms) because it only probes node_modules metadata and bun.lockb — it never opens the lockfile. On empty fixtures, detect without stopDir walked parents and took about 1.62 ms here; with stopDir it stayed about 0.65 ms and returned null. Process-agent checks were sub-microsecond: getUserAgent about 0.00045 ms vs whichPMRuns about 0.00081 ms with a set user agent. For spawning the resolved PM next, see tinyexec vs execa; for dead-dependency cleanup after installs, knip vs depcheck.
What does each one cost to install and bundle?
| Package / entry | Minified | gzip | Input files | Alone on disk |
|---|---|---|---|---|
| package-manager-detector (detect + getUserAgent + resolveCommand) | 8,196 B | 2,501 B | 5 | 84 KB (1 package) |
| pmd detect-only | 2,728 B | 1,114 B | 3 | — |
| which-pm | 56,056 B | 18,635 B | 9 | 1,400 KB (which-pm + load-yaml-file tree) |
| which-pm-runs | 260 B | 200 B | 2 | 28 KB |
package-manager-detector is zero-dependency and about 7× smaller gzip than which-pm's bundled graph here. which-pm-runs is a separate tiny package for the process agent (closest sibling to getUserAgent). Size pressure in CI is the same theme as size-limit vs bundlesize; path joins while walking parents pair with pathe vs node:path.
Do they behave the same?
| Fixture (stopDir = fixture) | package-manager-detector | which-pm |
|---|---|---|
| package-lock.json | { name: npm, agent: npm } | null |
| pnpm-lock.yaml | { name: pnpm, agent: pnpm } | null |
| yarn.lock | { name: yarn, agent: yarn } | null |
| empty package.json | null | null |
| node_modules only | null | { name: npm } |
| packageManager: pnpm@9.12.0 | pnpm agent + version 9.12.0 | null |
| bun.lock + bun.lockb | { name: bun, agent: bun } | { name: bun } |
| npm_config_user_agent=pnpm/... | getUserAgent → pnpm | whichPMRuns → { name: pnpm, version: 9.12.0 } |
| resolveCommand(pnpm, add, [lodash]) | { command: pnpm, args: [add, lodash] } | N/A |
| engines.node | unset | >= 22.13 (ran on 20.19.2) |
The important gap: which-pm inspects installation metadata under node_modules (and bun.lockb), so a fresh repo with only a lockfile returns null until you install. package-manager-detector reads lockfiles and package.json fields and can walk parents unless you set stopDir. Agent lists: 11 agents and 12 lock names in pmd constants on this version. Logging around install scripts is covered in consola vs pino; globbing lockfiles in a monorepo pairs with fast-glob vs globby. For shipping a small detector CLI, tsup vs unbuild and picocolors vs chalk are the usual neighbors.
Which one should you pick?
| Situation | My pick |
|---|---|
| Detect PM from lockfile / packageManager field before install | package-manager-detector |
| Detect what already filled node_modules | which-pm |
| Need resolveCommand / constructCommand for add/run | package-manager-detector |
| Only read npm_config_user_agent | getUserAgent or which-pm-runs |
| Smallest install + gzip for lockfile detect | package-manager-detector |
| Hot path that only checks node_modules | which-pm (about 11× here) |
Bottom line: on this box package-manager-detector 1.9.0 took about 0.49 ms to detect npm from package-lock.json while which-pm 4.0.0 finished in about 0.043 ms but returned null until node_modules existed. Gzip favored pmd at 2.5 KB against which-pm's 18.6 KB. Who should not switch to which-pm: scaffolders and CI that must choose a PM from the lockfile alone. Who should not pull pmd for a post-install check: scripts that only need to know what already landed in node_modules and want the cheaper probe.
How this was made: I installed the packages on the ShopperCove box, built seven fixtures, timed detect (with and without stopDir) vs whichPM over 200 calls × 9 rounds in two processes, compared getUserAgent to whichPMRuns, bundled each entry with esbuild, and recorded install sizes. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/antfu-collective/package-manager-detector
- https://github.com/zkochan/packages/tree/main/which-pm
- https://github.com/zkochan/packages/tree/main/which-pm-runs
- https://www.npmjs.com/package/package-manager-detector
- https://www.npmjs.com/package/which-pm
- https://esbuild.github.io/
Related
- https://www.shoppercove.com/blog/tinyexec-vs-execa
- https://www.shoppercove.com/blog/knip-vs-depcheck
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/pathe-vs-node-path
- https://www.shoppercove.com/blog/consola-vs-pino
- https://www.shoppercove.com/blog/fast-glob-vs-globby
- https://www.shoppercove.com/blog/tsup-vs-unbuild
- https://www.shoppercove.com/blog/picocolors-vs-chalk
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~03:55 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 20.19.2). package-manager-detector 1.9.0 vs which-pm 4.0.0 vs which-pm-runs 2.0.0; esbuild 0.28.2. Fixtures: npm/pnpm/yarn/bun locks, empty, nm-only, packageManager field. Median of 9 × 200 (ms, stopDir): pmd 0.493/0.407/0.466/0.236 vs which-pm ~0.043 across locks; empty 0.645 vs 0.045. Behavior: lockfile-only → pmd name+agent, which-pm null; nm-only → which-pm npm, pmd null with stopDir; getUserAgent pnpm vs whichPMRuns {name,version}. Bundle gzip 2,501 vs 18,635 B; install 84 vs 1,400 KB. engines.node >=22.13 on which-pm packages, ran on 20.19.2. Not tested: yarn Berry beyond integrity, Windows, Bun/Deno hosts. No affiliate.
Related links
Plate 68
pnpm vs npm vs Bun: Which Installs Fastest? 14s, 3.1s, 1.5s
npm 12.2.0 vs pnpm 12.9.1 vs Bun 1.4.2 on one 24-dependency React + Vite app: from scratch 14.2 s / 3.1 s / 1.5 s; lockfile + warm cache 2.9 s / 0.28 s / 0.27 s; lockfile + empty cache 3.9 s / 2.3 s / 0.45 s. Disk, lockfile size and the pnpm phantom-dependency catch.
5 Oct 2026
Plate 71
find-up vs locate-path: 47 µs Sync vs 596 µs Async
5 Oct 2026
Plate 97
c12 vs cosmiconfig: 0.58 ms vs 1.08 ms per .mjs Load
5 Oct 2026