Search across all documentation pages
The practical differences between pnpm and npm — how they install packages, manage disk space, handle lockfiles, and when to pick one over the other.
Quick-reference recipe card — copy-paste ready.
# Install pnpm (requires Node.js 18.12+)
corepack enable
corepack prepare pnpm@latest --activate
# Or install via npm
npm install -g pnpm
# Verify
pnpm --version
# Common commands — side by side
npm install # pnpm install
npm install react # pnpm add react
npm install -D vitest # pnpm add -D vitest
npm uninstall react # pnpm remove react
npm run dev # pnpm dev (or pnpm run dev)
npm test # pnpm test
npx create-next-app # pnpm create next-app (or pnpm dlx create-next-app)When to reach for this: When starting a new project or evaluating whether to switch from npm to pnpm. Understanding the tradeoffs helps you make an informed choice rather than switching based on hype.
npm uses a flat node_modules structure. When you run npm install, it:
package.json for direct dependencies.node_modules/ to reduce duplication.package-lock.json to lock exact versions.node_modules/
react/
react-dom/
scheduler/ # dependency of react-dom, hoisted to top level
loose-envify/ # transitive dependency, also hoisted
The problem with hoisting: your code can import packages that are not in your package.json because they happen to be hoisted. This is called phantom dependencies — your code works by accident until someone removes the package that pulled in the transitive dependency.
pnpm uses a content-addressable store and symlinks. When you run pnpm install, it:
~/.local/share/pnpm/store/ on macOS/Linux).node_modules/.pnpm/ with hard links to the global store.node_modules/ that point only to your direct dependencies.node_modules/
react -> .pnpm/react@19.1.0/node_modules/react
react-dom -> .pnpm/react-dom@19.1.0/node_modules/react-dom
.pnpm/
react@19.1.0/
node_modules/
react/ # hard link to global store
react-dom@19.1.0/
node_modules/
react-dom/ # hard link to global store
scheduler/ # symlink to .pnpm/scheduler@.../
This layout means:
package.json — no phantom dependencies.node_modules is smaller and installs are faster because files are linked, not copied.| Feature | npm | pnpm |
|---|---|---|
| node_modules structure | Flat, hoisted | Symlinked, strict |
| Disk usage | Full copy per project | Content-addressable store, hard links |
| Phantom dependencies | Allowed (hoisting) | Blocked by default |
| Install speed | Moderate | Faster (links instead of copies) |
| Lockfile | package-lock.json | pnpm-lock.yaml |
| Monorepo support | Workspaces (npm 7+) | Workspaces + pnpm-workspace.yaml |
| Ships with Node.js | Yes | No (install separately or via Corepack) |
| npx equivalent | npx | pnpm dlx |
| Run scripts shorthand | npm run dev | pnpm dev |
| Peer dependency handling | Auto-installed (npm 7+) | Strict — warns or errors on missing peers |
| Corepack support | n/a (bundled) |
With npm, if you have 10 projects that all use React 19, React is copied 10 times. With pnpm, React 19 is stored once in the global store and hard-linked into each project. On a machine with many projects, pnpm can save gigabytes of disk space.
# See how much space pnpm's store is using
pnpm store status
# Clean up unreferenced packages from the store
pnpm store prunepnpm is consistently faster than npm for several reasons:
Typical benchmarks show pnpm 2-3x faster than npm on cold installs and significantly faster on warm installs (when packages are already in the store).
pnpm's strict node_modules layout catches real bugs:
// This works with npm (phantom dependency) but fails with pnpm:
import chalk from 'chalk';
// Error: chalk is not listed in package.jsonIf your code only works because of npm's hoisting, pnpm will surface the missing dependency immediately. This is a feature, not a bug — it means your package.json is an accurate description of what your project actually uses.
Both npm and pnpm support workspaces, but pnpm's implementation is more mature:
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'# Run a command in a specific workspace
pnpm --filter @myapp/web dev
# Install a dependency in a specific workspace
pnpm --filter @myapp/web add zod
# Run a command across all workspaces
pnpm -r buildpnpm's --filter flag is more powerful than npm's --workspace flag, supporting glob patterns, dependency-based filtering, and changed-since-commit filtering.
| Task | npm | pnpm |
|---|---|---|
| Install all deps | npm install | pnpm install |
| Add a package | npm install zod | pnpm add zod |
| Add a dev dep | npm install -D vitest | pnpm add -D vitest |
| Remove a package | npm uninstall zod | pnpm remove zod |
| Run a script | npm run dev | pnpm dev |
| Run tests | npm test | pnpm test |
| Execute a binary | npx create-next-app | pnpm dlx create-next-app |
| Update deps | npm update | pnpm update |
| Audit for vulns | npm audit | pnpm audit |
| List installed | npm ls | pnpm ls |
| Global install | npm install -g tsx | pnpm add -g tsx |
# 1. Install pnpm
corepack enable
corepack prepare pnpm@latest --activate
# 2. Delete npm artifacts
rm -rf node_modules package-lock.json
# 3. Install with pnpm
pnpm install
# 4. (Optional) Pin pnpm version in package.json
# Add: "packageManager": "pnpm@9.15.4"
# 5. Update your scripts — npm run -> pnpm
# Update CI config (GitHub Actions, etc.)The migration is usually painless. If it fails, it is almost always because of phantom dependencies — packages your code imports but that are missing from package.json. Fix them by adding the missing packages explicitly.
node_modules and package-lock.json."packageManager" in package.json ensures everyone uses the same tool and version.package.json, it works with npm but fails with pnpm. The fix is to add the missing package to package.json — this is the correct thing to do anyway.node_modules. Older packages that use require.resolve tricks or walk up the directory tree may fail with pnpm's symlinked layout. The workaround is node-linker=hoisted in .npmrc, but this defeats pnpm's strictness.package-lock.json (JSON). pnpm uses pnpm-lock.yaml (YAML). You cannot use both — pick one and add the other to .gitignore.npx does not exist in pnpm. Use pnpm dlx instead. Example: pnpm dlx create-next-app instead of npx create-next-app.react as a peer dependency and you have not installed it, pnpm warns or errors. Fix by installing the peer dependency.pnpm/action-setup or Corepack to install pnpm before running pnpm install.pnpm store prune periodically to clean up packages no longer referenced by any project.| Tool | Strengths | Weaknesses |
|---|---|---|
| npm | Ships with Node.js, widest ecosystem support | Flat hoisting, phantom deps, slower installs |
| pnpm | Fast, strict, disk efficient, great monorepo support | Extra install step, stricter may break legacy packages |
| Yarn Classic (1.x) | Stable, widely used | Maintenance mode, similar hoisting issues as npm |
| Yarn Berry (3+) | Plug'n'Play, zero-installs | Steep learning curve, compatibility issues, PnP can confuse tools |
| Bun | Extremely fast installs, runtime + bundler | Newer, not 100% Node.js compatible, smaller ecosystem |
You should not. Each has its own lockfile format (package-lock.json vs pnpm-lock.yaml) and node_modules structure. Pick one and commit only its lockfile. Add the other's lockfile to .gitignore.
Yes. Next.js works with pnpm out of the box. pnpm create next-app scaffolds a new project. The only caveat is that some Next.js examples use npm commands in their READMEs — just substitute with pnpm equivalents.
Corepack is a tool bundled with Node.js (18.17+) that manages package manager versions. When you set "packageManager": "pnpm@9.15.4" in package.json, Corepack ensures everyone uses exactly that version. Enable it with corepack enable.
Yes, in most benchmarks. pnpm is 2-3x faster on cold installs and even faster on warm installs (when packages are already in the global store). The speed comes from hard linking files instead of copying them and from parallel network requests.
Packages that your code imports but that are not listed in your package.json. They work with npm because hoisting makes transitive dependencies accessible at the top level. pnpm blocks this by default, which catches real bugs — your code should only import packages you explicitly depend on.
Use pnpm dlx. For example: pnpm dlx create-next-app instead of npx create-next-app. The dlx command downloads and runs the package without installing it permanently.
Yes. Delete node_modules and pnpm-lock.yaml, then run npm install. This generates a fresh package-lock.json. If you had phantom dependencies that pnpm caught, they will silently "work" again under npm.
Yes. Configure the registry in .npmrc the same way you would for npm: registry=https://your-registry.example.com/. pnpm reads .npmrc files.
Yarn Classic (1.x) is similar to npm with flat hoisting. Yarn Berry (3+) uses Plug'n'Play which avoids node_modules entirely but has compatibility issues with some packages. pnpm sits in the middle — strict and fast without the compatibility headaches of PnP.
If npm is working fine and your team is comfortable, there is no urgency. Consider switching if you are starting a new project, running a monorepo, or experiencing slow installs and disk space pressure. The migration is usually straightforward.
Yes — "packageManager": "pnpm@9.x" in package.json |