跪拜 Guibai
← All articles
Frontend · Frontend Engineering

npm, Yarn, or pnpm: The Disk and Speed Tradeoffs That Actually Matter

By 皮蛋小精灵 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Phantom dependencies are a time bomb in any npm or Yarn v1 project: code silently relies on packages that could vanish with the next minor upgrade. pnpm makes that class of bug impossible and cuts CI install times enough to matter on paid runner minutes.

Summary

The three major Node.js package managers differ less in their CLI commands than in how they store dependencies on disk — a choice that cascades into install speed, CI costs, and a whole class of subtle runtime bugs. npm and Yarn v1 both flatten node_modules, which hoists transitive dependencies to the top level and lets code import packages never declared in package.json. Those phantom dependencies work until an upstream package drops them, at which point the build breaks with no obvious cause.

pnpm replaces the flat node_modules model with a global content-addressable store and hard links. Each package version lives on disk exactly once, regardless of how many projects use it. More importantly, pnpm enforces strict dependency boundaries: a package can only access what it explicitly declares, so phantom imports fail at install time rather than surfacing as production errors months later.

Benchmarks on a ~200-dependency project show pnpm cutting cold-install times nearly in half versus npm, and repeated cached installs dropping to 3 seconds. In monorepos, pnpm workspaces share a single copy of each dependency across all packages, and its `--filter` flag runs only the packages affected by a change — a direct reduction in CI minutes and disk bills.

Takeaways
pnpm stores every package version exactly once in a global content-addressable store and links it into projects via hard links, so 10 projects sharing React 18 use ~1.1× the disk space instead of 10×.
npm and Yarn v1 both flatten node_modules, which hoists transitive dependencies to the top level and lets code import packages never listed in package.json — a phantom dependency that breaks silently when the upstream package changes.
pnpm's strict mode blocks access to undeclared dependencies at install time, forcing them into package.json where they belong.
In a ~200-dependency project, pnpm v9 cold-installs in ~40s versus ~85s for npm v10; cached installs with an unchanged lockfile drop to ~3s.
CI pipelines using pnpm can cache the global store directory and get near-instant incremental installs even on fresh runner containers.
pnpm workspaces share a single on-disk copy of each dependency across all packages; npm and Yarn workspaces still duplicate copies per workspace.
The `pnpm --filter` flag supports dependency-graph-aware scoping, so only packages affected by a change get rebuilt.
Migrating from npm or Yarn to pnpm mostly means auditing for phantom dependencies with `pnpm why` and replacing `npx` with `pnpm dlx`.
Yarn Berry's PnP mode eliminates node_modules entirely but still has spotty ecosystem support across Webpack, Vite, and CLI tools.
Vite, Vue, Nx, and Turborepo all use pnpm — it is the de facto default for new frontend monorepos.
Conclusions

The phantom dependency problem is more dangerous than most teams realize: it doesn't just break builds, it can silently change runtime behavior when a transitive dependency bumps a patch version, with no error and no obvious place to look.

pnpm's hard-link model is the only one of the three that makes monorepo dependency sharing a physical reality rather than a hoisting heuristic — the disk savings are real, not just logical deduplication.

Yarn Berry's PnP is technically the most radical and correct approach, but its ecosystem incompatibility has made it a non-starter for most teams, illustrating that package management is as much a social coordination problem as a technical one.

The migration cost from npm or Yarn to pnpm is dominated by phantom dependency cleanup, which is actually a latent quality audit that would benefit the project regardless of the package manager switch.

Concepts & terms
Phantom dependency
A package that code can import even though it is not declared in package.json, because a flat node_modules structure hoists transitive dependencies to the top level. The import works until the parent package drops or changes the dependency, at which point the build breaks or behavior shifts silently.
Content-addressable store
A storage model where files are identified by a hash of their content rather than by name or path. pnpm uses this so that identical package versions across projects point to the same bytes on disk via hard links, eliminating duplication.
Hard link
A filesystem entry that points directly to the same inode (data blocks) as another file. Multiple hard links to the same file share identical content and metadata; deleting one link does not remove the data until all links are gone. pnpm uses hard links to map its global store into each project's node_modules.
PnP (Plug'n'Play)
Yarn Berry's alternative to node_modules that replaces the filesystem-based resolution with a .pnp.cjs mapping file. It resolves imports without traversing the filesystem, but many tools that expect a physical node_modules directory are incompatible with it.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗