# Install pnpm
npm install -g pnpm
# Migrate existing project
pnpm import && pnpm installYou run npm install and wait. And wait. The progress bar crawls. You check your node_modules folder. It is 800MB for a project with 12 dependencies.
You have three projects on your machine that all use React 18.2.0. npm dutifully stores three separate copies. 150MB times three. You are paying for the same bytes three times over.
This is the npm tax. Every JavaScript developer pays it. Most do not realize there is an alternative.
pnpm fixes this. It is faster, uses less disk space, and catches dependency bugs that npm silently ignores. The migration takes five minutes.
#How pnpm Works
pnpm stores every package version exactly once on your machine, in a global content-addressable store. When you install a package, pnpm creates a hard link from that store to your project's node_modules.
# Global store (one copy per version)
~/.pnpm-store/
v3/
files/
[email protected]/
[email protected]/
[email protected]/
# Project A (hard links, not copies)
~/projects/project-a/node_modules/
react -> ~/.pnpm-store/v3/files/[email protected]/
lodash -> ~/.pnpm-store/v3/files/[email protected]/
# Project B (same links)
~/projects/project-b/node_modules/
react -> ~/.pnpm-store/v3/files/[email protected]/
Ten projects using React 18.2.0? One copy on disk. Hard links are instant and take no additional space. Your node_modules folder becomes a directory of pointers.
The store is content-addressable. This means packages are identified by their content hash, not just their name and version. If two different packages contain the same file, pnpm stores it once.
#Speed Comparison
I tested both package managers on a real project with 847 dependencies. Clean install, no cache.
| Scenario | npm | pnpm | Difference |
|---|---|---|---|
| Clean install (cold cache) | 42s | 18s | 2.3x faster |
| Clean install (warm cache) | 38s | 8s | 4.7x faster |
| Adding one package | 12s | 3s | 4x faster |
| node_modules size | 412MB | 178MB | 2.3x smaller |
The warm cache is where pnpm really shines. Once a package is in your global store, installing it in a new project is nearly instant. You are not downloading anything. You are creating links.
#The Phantom Dependency Problem
npm has a silent bug factory called flat node_modules. When you install a package, npm also installs all its dependencies and puts them at the top level. Your code can import these transitive dependencies even though you did not explicitly install them.
// package.json only lists "express"
// But express depends on "cookie", so npm installs it
// This works! But it shouldn't.
import cookie from 'cookie';This is a phantom dependency. Your code depends on something that is not in your package.json. It works until express decides to use a different cookie library, or pins a version incompatible with your usage. Then your build breaks and you have no idea why.
pnpm fixes this with a non-flat node_modules structure. Your node_modules only contains packages you explicitly installed. Transitive dependencies are nested.
# pnpm node_modules structure
node_modules/
.pnpm/ # All packages live here
[email protected]/
node_modules/
express/
cookie/ # express's dependency
[email protected]/
node_modules/
cookie/
express -> .pnpm/[email protected]/node_modules/express
# No direct link to cookie!
Try to import cookie without adding it to your package.json? pnpm fails immediately.
Error: Cannot find module 'cookie'
This catches bugs before they reach production. You will thank pnpm when you discover a phantom dependency that has been silently working for months.
#Strict Mode
pnpm's strictness can be configured. The default is already stricter than npm, but you can go further.
# .npmrc
strict-peer-dependencies=true
auto-install-peers=falseWith strict peer dependencies, pnpm fails if peer dependency requirements are not met. npm just warns. This surfaces version conflicts immediately instead of letting them cause mysterious runtime bugs.
#Workspace Support for Monorepos
pnpm has first-class monorepo support with workspaces. The configuration is simpler than npm workspaces and more powerful.
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'That is the entire configuration. pnpm automatically discovers all packages in those directories.
#Installing Dependencies
# Install everything in all workspaces
pnpm install
# Add a dependency to a specific workspace
pnpm add lodash --filter @myapp/web
# Add a workspace package as a dependency
pnpm add @myapp/shared --filter @myapp/web --workspaceThe --filter flag is powerful. It accepts package names, directory paths, and glob patterns.
# Run tests in all packages that changed since main
pnpm test --filter '...[main]'
# Build all packages that depend on @myapp/core
pnpm build --filter '...@myapp/core'
# Run in all packages under apps/
pnpm dev --filter './apps/*'#Workspace Protocol
pnpm uses a workspace protocol for local packages.
// apps/web/package.json
{
"dependencies": {
"@myapp/shared": "workspace:*",
"@myapp/ui": "workspace:^1.0.0"
}
}workspace:* always uses the local version. workspace:^1.0.0 uses the local version during development but publishes as ^1.0.0.
When you publish, pnpm replaces the workspace protocol with actual version numbers. Your published package has normal dependency declarations.
#Migration Guide
Switching from npm takes five minutes.
#Step 1: Install pnpm
# Using npm (ironic but works)
npm install -g pnpm
# Or using corepack (built into Node 16.13+)
corepack enable
corepack prepare pnpm@latest --activate
# Or standalone script
curl -fsSL https://get.pnpm.io/install.sh | sh -#Step 2: Import Existing Lock File
# Navigate to your project
cd my-project
# Delete node_modules
rm -rf node_modules
# Import package-lock.json to pnpm-lock.yaml
pnpm import
# Install with pnpm
pnpm installpnpm reads your package-lock.json and creates an equivalent pnpm-lock.yaml. Your dependencies stay exactly the same.
#Step 3: Update CI/CD
Replace npm commands with pnpm equivalents.
# Before (GitHub Actions)
- run: npm ci
- run: npm test
- run: npm run build
# After
- uses: pnpm/action-setup@v2
with:
version: 8
- run: pnpm install --frozen-lockfile
- run: pnpm test
- run: pnpm run build#Step 4: Add .npmrc (Optional)
Create an .npmrc file to configure pnpm behavior.
# .npmrc
shamefully-hoist=false
strict-peer-dependencies=true
auto-install-peers=trueshamefully-hoist=true flattens node_modules like npm. Use this only if you have compatibility issues. The name is intentional. pnpm is shaming you for needing it.
#Step 5: Update .gitignore
node_modules
pnpm-lock.yaml # Or commit it, depending on your preferenceMost teams commit the lock file. It ensures reproducible builds.
#Command Comparison
pnpm commands mirror npm closely. The transition is intuitive.
| npm | pnpm | Notes |
|---|---|---|
| npm install | pnpm install | Same |
| npm install lodash | pnpm add lodash | add instead of install |
| npm uninstall lodash | pnpm remove lodash | remove instead of uninstall |
| npm run build | pnpm build | No run needed for scripts |
| npm ci | pnpm install --frozen-lockfile | Fails if lock file needs update |
| npm update | pnpm update | Same |
| npx create-react-app | pnpm dlx create-react-app | dlx instead of npx |
One convenience: pnpm build is shorthand for pnpm run build. You can drop the run for any script in your package.json.
#When to Stick with npm
pnpm is not always the right choice.
Stay with npm if:
- Your team is not ready to learn a new tool
- You have CI/CD that is hard to modify
- You depend on packages with known pnpm incompatibilities
- You need native npm features like provenance attestations
Consider npm for:
- Simple projects with few dependencies
- Teaching beginners who will encounter npm elsewhere
- Organizations with strict tooling policies
npm is fine. It works. But if you install packages frequently, work on multiple projects, or maintain monorepos, pnpm pays for itself immediately.
#When to Use Yarn
Yarn is the other alternative. Here is when it makes sense.
Use Yarn if:
- You need Plug'n'Play (PnP) for even stricter dependency resolution
- Your team already uses it
- You want zero-installs in your repo
Use pnpm over Yarn if:
- You want simpler configuration
- You prefer a flatter learning curve
- You do not need PnP
Yarn Berry (v2+) is powerful but complex. pnpm offers most of the benefits with less friction.
#Summary
pnpm is npm but faster and smarter. It stores packages once globally and links them into your projects. This saves disk space and installation time.
Key benefits:
- 2-4x faster installs
- 50% less disk usage
- Catches phantom dependencies
- Native monorepo support
Key commands:
pnpm addinstead ofnpm install <package>pnpm install --frozen-lockfileinstead ofnpm cipnpm dlxinstead ofnpx
Migration:
rm -rf node_modules
pnpm import
pnpm installStop paying the npm tax. Switch to pnpm.