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.

  • Node.js
  • npm
  • pnpm