All three install packages from the same npm registry. The differences are in how they store packages, how fast they are, how strict they are about dependencies, and how well they handle monorepos.
Quick comparison
| npm | Yarn (Berry, v4) | pnpm | |
|---|---|---|---|
| Ships with Node.js | Yes | Via Corepack | Via Corepack |
| node_modules layout | Flat (hoisted) | Plug’n’Play by default, or node_modules | Symlinked, content-addressable store |
| Disk usage across projects | Duplicated per project | Cached; PnP avoids node_modules | Shared global store, hard links |
| Install speed | Good | Fast | Very fast |
| Strictness (phantom deps) | Lenient | Strict with PnP | Strict |
| Workspaces / monorepos | Yes | Excellent | Excellent |
| Lockfile | package-lock.json |
yarn.lock |
pnpm-lock.yaml |
| CI install | npm ci |
yarn install --immutable |
pnpm install --frozen-lockfile |
npm
The default — always available, no setup. Modern npm is reliable and reasonably fast, and npm ci gives reproducible installs from the lockfile. Its flat node_modules lets code import packages it didn’t declare (“phantom dependencies”), which can break when dependency trees change.
Choose npm when you want zero extra tooling, or the project already uses it.
pnpm
pnpm stores each package version once on disk in a global content-addressable store and hard-links it into projects. node_modules contains only your declared dependencies (with symlinks), so undeclared imports fail fast.
- Much less disk space on servers or CI runners with many projects.
- Very fast installs, especially with a warm store.
- Excellent workspace support for monorepos.
A few older tools assume a flat node_modules; node-linker=hoisted in .npmrc is the escape hatch.
Yarn
Yarn “Berry” (v2+) introduced Plug’n’Play: no node_modules at all — a single resolution map tells Node where packages are. It’s fast and strict, but some tools need adaptation. Yarn can also use a traditional node_modules linker. Yarn 1 (“Classic”) is in maintenance mode; many older projects still use it.
Corepack: pin the package manager per project
Corepack (bundled with Node.js 22 and earlier; installable via npm for newer Node versions) reads the packageManager field in package.json and runs exactly that version:
{
"packageManager": "[email protected]"
}
corepack enable
pnpm install # Corepack provides pnpm 9.15.0 automatically
Everyone — developers, CI and servers — uses the same version, avoiding lockfile churn.
Lockfiles: commit them
Whichever you choose, commit the lockfile and use the frozen/clean install command in CI and production. Mixing managers in one project (an npm install here, a yarn there) produces conflicting lockfiles — pick one and add a note to the README.
On servers and in CI
- Install with the frozen-lockfile command and production-only dependencies:
npm ci --omit=dev
pnpm install --frozen-lockfile --prod
yarn workspaces focus --production # Yarn Berry with the workspace-tools plugin
- Cache the package manager’s store between CI runs (
~/.npm, pnpm’s store, Yarn’s cache) — see Docker build cache for Docker builds. - On a server hosting several Node apps, pnpm’s shared store saves noticeable disk space.
Migrating
pnpm import # create pnpm-lock.yaml from package-lock.json or yarn.lock
rm package-lock.json
pnpm install
Run the test suite afterwards: stricter layouts expose undeclared dependencies, which you then add to package.json.
Recommendation
- Single app, small team: npm is fine.
- Monorepo, many projects, CI speed or disk space matters: pnpm.
- Already on Yarn Berry and happy: keep it.
Related: installing Node.js on Ubuntu and Bun, whose built-in package manager is another fast option.