npm, pnpm, and yarn are package managers for the JavaScript ecosystem, responsible for installing, managing, and resolving dependencies in Node.js projects. While npm is the default package manager bundled with Node.js, yarn (originally developed by Facebook) and pnpm emerged as alternatives offering improved performance, stricter dependency resolution, and enhanced developer workflows—particularly in monorepo environments. All three support standard npm registry protocols, lock files for deterministic installs, and workspace features for managing multi-package repositories, but they differ significantly in how they store dependencies, handle disk usage, enforce dependency isolation, and optimize installation speed.
When building modern JavaScript applications, your choice of package manager isn’t just about installing dependencies — it affects disk usage, build performance, team consistency, and even how you structure monorepos. While npm, pnpm, and yarn all solve the same core problem (managing packages), they do so with fundamentally different approaches under the hood. Let’s break down what really matters in production-grade workflows.
This is where the biggest architectural divergence happens — and it impacts everything from CI speed to local disk space.
npm uses a nested node_modules structure by default (though it tries to flatten when possible). This can lead to duplicate packages and “phantom dependencies” — where your code accidentally uses a sub-dependency that wasn’t declared in your package.json.
# npm install creates deeply nested folders
node_modules/
lodash/
express/
node_modules/
lodash/ # ← possible duplicate!
yarn (classic and Berry) uses a flat node_modules by default, which avoids nesting but still copies every package into your project. This prevents phantom dependencies better than npm’s old behavior, but still duplicates files across projects.
# yarn install (classic)
yarn add lodash
# → copies full lodash into node_modules/lodash
pnpm takes a radically different approach: it uses a content-addressable store and hard links. Every package version is stored once globally, and your node_modules contains only hard links pointing to that central store. This saves massive disk space and speeds up installs.
# pnpm install
pnpm add lodash
# → creates hard link from global store to node_modules/.pnpm/lodash@x.x.x
💡 Real-world impact: In a monorepo with 50 packages sharing React 18,
pnpmstores React once;npmandyarnmay store it 50 times.
All three generate lock files, but their behavior differs in subtle but important ways.
npm uses package-lock.json. It locks exact versions and dependency tree structure. However, npm’s resolver can sometimes produce non-deterministic trees across machines if package.json uses loose semver ranges.
// package-lock.json (npm)
{
"name": "my-app",
"lockfileVersion": 3,
"packages": {
"": { "dependencies": { "lodash": "^4.17.0" } },
"node_modules/lodash": { "version": "4.17.21" }
}
}
yarn uses yarn.lock. It’s highly deterministic and includes integrity hashes. Yarn Berry (v2+) also supports zero-installs via .yarn/cache, letting you commit cached packages to Git.
# yarn.lock (Yarn Berry)
lodash@^4.17.0:
version: 4.17.21
resolution: "lodash@4.17.21"
checksum: 9d3a0e9...
pnpm uses pnpm-lock.yaml. It’s fully deterministic and includes a strict dependency graph. Crucially, pnpm enforces strict dependency isolation: you can only require packages explicitly listed in your package.json — no accidental access to transitive deps.
# pnpm-lock.yaml
lockfileVersion: '6.0'
importers:
.:
dependencies:
lodash:
specifier: ^4.17.0
version: 4.17.21
⚠️ Phantom dependency trap: With
npmoryarn,require('debug')might work even if you never installed it — because a sub-dependency did. Withpnpm, this fails at runtime, forcing clean dependency declarations.
All three support workspaces, but their ergonomics vary.
npm added workspaces in v7. You define them in package.json:
// package.json (npm workspaces)
{
"workspaces": ["packages/*"]
}
Then run scripts across packages:
npm run build --workspaces
yarn has first-class workspace support since v1. Yarn Berry enhances this with protocols like workspace:* for linking local packages:
// package.json (Yarn Berry)
{
"dependencies": {
"my-utils": "workspace:*"
}
}
pnpm also supports workspaces with similar syntax:
# pnpm-workspace.yaml
packages:
- 'packages/*'
And uses workspace:* protocol:
// package.json (pnpm)
{
"dependencies": {
"my-utils": "workspace:*"
}
}
💡 Key difference:
pnpmandyarnsupport protocol-based versioning (workspace:*), whilenpmrequires manual version syncing or external tooling.
In real-world benchmarks:
pnpm wins by a huge margin due to hard links. A typical monorepo might use 2GB with pnpm vs 10GB+ with npm/yarn.pnpm and yarn (with cache) are generally faster than npm, especially on repeat installs.pnpm’s store can be cached effectively. yarn’s zero-installs let you skip yarn install entirely in CI if you commit the cache.Example: Installing a large project with 1000+ deps:
# pnpm (uses global store + hard links)
time pnpm install # ~8s
# yarn (copies all files)
time yarn install # ~15s
# npm (nested resolution)
time npm install # ~20s
(Times are illustrative; actual results depend on network, cache, and project size.)
npm is bundled with Node.js, so it’s always available. Its CLI is simple but lacks advanced features like interactive upgrades or built-in patching.
npm outdated
npm update lodash
yarn (especially Berry) offers rich DX: interactive upgrade CLI, constraints engine for linting dependencies, and PnP (Plug’n’Play) mode that skips node_modules entirely.
yarn upgrade-interactive
yarn dlx create-react-app # runs without installing
pnpm provides excellent monorepo tooling (pnpm -r exec), strictness by default, and seamless integration with modern bundlers. It also supports .npmrc configuration like npm.
pnpm -r build # runs 'build' in all workspace packages
pnpm add -D typescript --filter ./packages/ui
All three support npm-compatible registries (including private ones), but differ in retry logic and offline modes.
npm: Basic retry logic; offline mode requires --offline flag and pre-cached tarballs.yarn: Aggressive caching; offline mode works out of the box if cache exists.pnpm: Efficient store reuse; offline installs work if packages are in global store.Switching between them is usually safe, but watch for:
node_modules layout assumptions: Some tools (like older bundlers) assume flat node_modules. pnpm’s symlinked structure may break them (though rare in 2024)..gitignore and team policy.preinstall, etc.) behave slightly differently, especially around workspaces.npm if:yarn if:pnpm if:| Feature | npm | yarn (Berry) | pnpm |
|---|---|---|---|
| Storage Model | Nested/copy | Flat/copy | Hard links + global store |
| Phantom Deps | Possible | Avoided (mostly) | Strictly prevented |
| Lock File | package-lock.json | yarn.lock | pnpm-lock.yaml |
| Workspaces | Basic (v7+) | Advanced + PnP | Solid + filtering |
| Disk Efficiency | Low | Medium | High |
| Install Speed | Moderate | Fast (with cache) | Very fast |
| Zero-Installs | ❌ | ✅ | ❌ |
| Bundled with Node | ✅ | ❌ | ❌ |
There’s no universal “best” — only what fits your team’s workflow, project scale, and tolerance for complexity. But if you’re starting a new large-scale project or monorepo in 2024, pnpm offers the best blend of performance, correctness, and developer experience without the cognitive overhead of Yarn’s PnP or npm’s legacy quirks. For smaller teams or simpler apps, npm remains a perfectly valid default. And if you love Yarn’s rich tooling and don’t mind the extra config, it’s still a powerhouse.
Choose pnpm if you work on large projects or monorepos and care about disk efficiency, fast installations, and strict dependency isolation. Its content-addressable store and hard linking drastically reduce redundant package storage, while its strict node_modules structure prevents accidental use of undeclared dependencies—making it a strong choice for teams prioritizing correctness and performance.
Choose npm if you prioritize simplicity and minimal tooling overhead. It's ideal for small to medium projects where you don't need advanced monorepo features, and you want the default, universally available package manager that ships with Node.js. Avoid it if you're working in large monorepos where disk usage and phantom dependencies could become problematic.
Choose yarn if you want rich developer tooling like interactive upgrades, constraints validation, and zero-installs (via Yarn Berry). It's well-suited for teams that value advanced CLI features and are comfortable managing additional configuration. However, be aware that its Plug'n'Play mode can introduce compatibility issues with some tools that expect a traditional node_modules layout.
简体中文 | 日本語 | 한국어 | Italiano | Português Brasileiro
Fast, disk space efficient package manager:
node_modules are linked from a single content-addressable storage.package.json.pnpm-lock.yaml.To quote the Rush team:
Microsoft uses pnpm in Rush repos with hundreds of projects and hundreds of PRs per day, and we’ve found it to be very fast and reliable.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ⏱️ Time.now |
Support this project by becoming a sponsor.
pnpm uses a content-addressable filesystem to store all files from all module directories on a disk. When using npm, if you have 100 projects using lodash, you will have 100 copies of lodash on disk. With pnpm, lodash will be stored in a content-addressable storage, so:
pnpm update will only add 1 new file to the storage.As a result, you save gigabytes of space on your disk and you have a lot faster installations!
If you'd like more details about the unique node_modules structure that pnpm creates and
why it works fine with the Node.js ecosystem, read this small article: Flat node_modules is not the only way.
💖 Like this project? Let people know with a tweet
For installation options visit our website.
Just use pnpm in place of npm/Yarn. E.g., install dependencies via:
pnpm install
For more advanced usage, read pnpm CLI on our website, or run pnpm help.
pnpm is up to 2x faster than npm and Yarn classic. See all benchmarks here.
Benchmarks on an app with lots of dependencies: