Nitin Kumar SinghSolutions Architect

Type to search. ↑↓ to move, Enter to open.

    ↑↓ move ↵ open esc close
    Deep DiveJavaScript

    Why I Switched from npm to pnpm (And Why You Should Too)

    A practical breakdown of why pnpm beats npm for modern JS/TS projects — faster installs, strict dependency resolution, and monorepo support.

    If you write JavaScript or TypeScript professionally, you’ve used npm. It ships with Node.js, it’s everywhere, and for years it was the only game in town. But after running npm on real-world projects (enterprise monorepos, Next.js apps, Angular dashboards, CI pipelines that bill by the minute), I can tell you: npm is holding you back.

    I switched to pnpm about two years ago. I haven’t looked back. Here’s what actually changed.

    The Problem with npm (That Nobody Talks About)

    npm works. That’s never been the issue. The issue is how it works under the hood, and the problems that creates once your projects grow beyond a weekend hack.

    Flat node_modules Is a Lie

    npm hoists every dependency into a single flat node_modules folder. The catch: your code can import packages you never declared in package.json. This is called phantom dependencies.

    It works on your machine because npm happened to hoist that package. It works in your teammate’s environment because they have the same lock file. Then it breaks in production, or in a Docker build, or on a new developer’s machine, and nobody understands why.

    This isn’t a hypothetical. I’ve debugged this exact issue in production. Twice.

    Your Disk Is Full of Duplicates

    Have five Next.js projects on your machine? npm keeps five separate copies of React, five copies of TypeScript, five copies of every shared dependency. We’re talking hundreds of megabytes duplicated per project.

    On a 256GB MacBook, that adds up fast.

    CI Pipelines Are Slow (And You’re Paying For It)

    npm resolves and installs dependencies sequentially. On a CI runner with a cold cache, a large project can spend 30+ seconds just on npm install. Multiply that by every PR, every merge, every deployment. Those minutes turn into hours per month, and if you’re on a metered CI provider, hours turn into dollars.

    What pnpm Does Differently

    pnpm isn’t a wrapper around npm. It’s a fundamentally different approach to dependency management. Three things set it apart.

    1. Content-Addressable Storage

    When pnpm installs a package, it stores the files in a global content-addressable store on your disk, typically at ~/.local/share/pnpm/store. Every project that needs that package gets hard links to the same files. Not copies. Links.

    What this means in practice:

    • One copy of React on your entire machine, no matter how many projects use it.
    • If a new version changes 3 files out of 300, pnpm only downloads and stores those 3 new files.
    • Disk savings of 50–70% compared to npm are common. On a machine with many projects, I’ve seen savings over 80%.

    2. Non-Flat node_modules (Finally, Correctness)

    pnpm creates a symlinked node_modules structure where only your declared dependencies are accessible at the top level. Transitive dependencies are nested properly using symlinks to the .pnpm/ directory.

    This means:

    • No phantom dependencies. If you didn’t declare it in package.json, you can’t import it. Period.
    • Your code is honest about its dependencies. What works locally will work in CI, in Docker, in production.
    • Bugs that would have slipped through with npm get caught at development time, not deployment time.

    3. Parallel, Cached Installs

    pnpm resolves, fetches, and links dependencies in a streaming pipeline: all three stages overlap. npm does them sequentially.

    The official pnpm benchmarks:

    Scenarionpmpnpm
    Clean install (no cache, no lockfile)31.3s7.6s
    With cache + lockfile + node_modules1.2s546ms
    With cache + lockfile (CI scenario)7.5s2.0s
    With cache only12.1s5.4s

    That’s 2–4x faster across the board. On a monorepo with 10+ packages, the gap widens further.

    Monorepo Support That Actually Works

    npm added workspace support a few years ago, and it’s fine. It installs dependencies across packages. That’s about it.

    pnpm workspaces are in a different league:

    • pnpm --filter lets you run commands on specific packages, their dependencies, or their dependents. You can target by name, by directory, by changed files. It’s surgical.
    • workspace: protocol explicitly declares local package dependencies. No ambiguity about where a package resolves from: the registry or the local workspace.
    • Shared lockfile means one pnpm-lock.yaml at the root, with every dependency deduplicated across the entire monorepo.
    • Catalogs (introduced in pnpm 9.5) let you define shared version constraints across all workspace packages in one place.

    Catalogs: one version file for the whole monorepo

    Before catalogs, updating a shared dependency across a monorepo meant touching every package.json. With catalogs, you define versions once in pnpm-workspace.yaml:

    pnpm-workspace.yaml
    packages:
    - "apps/*"
    - "packages/*"
    catalog:
    react: ^18.3.1
    typescript: ^5.4.0
    zod: ^3.23.0

    Then reference them in any workspace package:

    {
    "dependencies": {
    "react": "catalog:",
    "zod": "catalog:"
    }
    }

    One version update in one file. Done. You can also define named catalogs for managing compatibility matrices across different environments.

    This isn’t theoretical. Look at who uses pnpm workspaces in production: Next.js, Vue, Nuxt, Vite, Turborepo, Prisma, Material UI, Astro, SvelteKit, Vitest, n8n, Qwik, and the list keeps going. These are not small projects.

    pnpm 10: Security by Default

    pnpm 10 (released December 2025) made security the default rather than an opt-in. Two features worth knowing:

    Script blocking by default: pnpm 10 prompts before running lifecycle scripts (postinstall, preinstall, etc.) from untrusted packages. Supply chain attacks via malicious install scripts are a real vector; this adds friction in the right place.

    minimumReleaseAge: You can configure pnpm to hold packages published less than 24 hours ago. This catches the window where attackers publish a malicious package and immediately target projects updating their dependencies.

    .npmrc
    minimum-release-age=24h

    npm has no equivalent defaults for either of these.

    The Migration Takes Five Minutes

    Switching from npm to pnpm is painless:

    Terminal window
    # Install pnpm globally
    npm install -g pnpm
    # Remove npm artifacts
    rm -rf node_modules package-lock.json
    # Install with pnpm
    pnpm install

    That’s it. Your package.json doesn’t change. Your code doesn’t change. pnpm reads the same package.json format, runs the same lifecycle scripts, and generates its own pnpm-lock.yaml.

    For a monorepo, add a pnpm-workspace.yaml at the root:

    packages:
    - "apps/*"
    - "packages/*"

    Locking the pnpm Version for Your Team

    Add a packageManager field to your package.json so everyone on the team uses the same pnpm version:

    {
    "packageManager": "pnpm@10.32.0"
    }

    Corepack (bundled with Node.js 14.19–24) enforces this automatically. Note: Corepack is being removed from Node.js 25+, so for new projects targeting Node 25 onward, use pnpm’s standalone installer and manage versions directly.

    Extra Features Worth Knowing

    pnpm patch patches a dependency inline without forking the repo. The patch is stored in your project as a .patch file and reapplied on every install. No more maintaining a fork for a two-line fix.

    pnpm env installs and switches Node.js versions without a separate tool. Useful in CI and Docker where you want everything managed by a single binary. For interactive local development where you switch between projects with different Node versions, you still want nvm or fnm for automatic switching via .nvmrc.

    Side-effects cache: if a package runs a postinstall build script (native compilation, etc.), pnpm caches the output. Reinstalls skip the build step entirely.

    pnpm clean (pnpm 10) safely removes node_modules across all workspace packages in one command.

    The One Caveat (And Its Fix)

    In practice, this is rare in 2026; most of the ecosystem has adapted. But if you hit it, there’s a quick workaround. Add this to your .npmrc:

    shamefully-hoist=true

    This tells pnpm to hoist dependencies like npm does. It defeats some of the strictness benefit, so treat it as a temporary fix while you identify the offending package.

    A better approach: use public-hoist-pattern to selectively hoist only what’s needed:

    public-hoist-pattern[]=*eslint*
    public-hoist-pattern[]=*prettier*

    Side-by-Side Comparison

    Featurenpmpnpm
    Storage modelCopies per projectContent-addressable store + hard links
    Disk usageDuplicates everywhere50–70% savings typical
    Install speedSequential resolutionParallel streaming pipeline (2–4x faster)
    node_modulesFlat (hoisting)Strict symlinked structure
    Phantom dependenciesSilently allowedBlocked by default
    Monorepo supportBasic workspacesFirst-class: filter, catalogs, workspace protocol
    Lock filepackage-lock.jsonpnpm-lock.yaml (more deterministic)
    Security defaultsNoneScript blocking, release age hold (pnpm 10)
    Patching dependenciesNot supportedBuilt-in pnpm patch
    Node.js version managementNot supportedpnpm env (good for CI; use nvm/fnm for interactive dev)
    Side-effects cacheNoYes

    When to Stick with npm

    Being fair: npm is still the right call in a few scenarios.

    • Tutorials and getting started: npm ships with Node, so new developers don’t need to install anything extra. For learning, the zero-setup matters.
    • Single small project, no monorepo: if you have one small app and disk space isn’t a concern, the benefits are less noticeable.
    • Team can’t adopt new tools: if your organization has strict tooling policies and adding pnpm to the approved list is a six-month process, just use npm.

    For everything else (enterprise projects, monorepos, CI-heavy workflows, teams that care about correctness), pnpm is the better default.

    Bottom Line

    npm is the Toyota Corolla of package managers. Reliable, available everywhere, gets the job done. pnpm is the same vehicle with a better engine, better fuel economy, and seatbelts that actually work.

    The major JavaScript frameworks have already made their choice. Next.js, Vue, Vite, Turborepo, Astro, SvelteKit: they all use pnpm. That’s not a coincidence.

    Try it on your next project. The migration is five minutes, the benefits are immediate, and the worst case is pnpm install works exactly like npm install did, only faster.

    Terminal window
    npm install -g pnpm

    That’s your starting line.

    Comments

    Comments are GitHub discussions. Sign in with GitHub to post; reactions need no account.