Why I’m Mass-Migrating Projects from npm to pnpm

For years I treated node_modules/ like a necessary evil: bloated, inconsistent, and occasionally cursed. Then I met pnpm and realized I’d been wasting time—and disk space—for reasons that weren’t technical at all. My npm projects worked… right up until they didn’t. pnpm fixes the problems I actually feel day to day: faster installs, stricter dependency behavior, and disk usage that doesn’t balloon quietly in the background.
The real problem with npm: it “works” until it doesn’t⌗
Let’s be honest: npm’s classic approach makes it easy to get moving, but it also makes it easy to accumulate technical debt without noticing. In many teams and many repos, node_modules becomes a behavioral dependency. A package might accidentally reach for a transitive dependency that happens to exist because of how npm flattened the tree. That’s not “functionality.” That’s coincidence.
The most frustrating bugs are the ones that only reproduce on “fresh” machines or after a clean install. You’ll see errors like:
Cannot find module 'x'- runtime failures caused by missing nested packages
- tests that pass locally because the dependency was present “somewhere” in the flattened structure
In other words: npm can mask dependency issues rather than surfacing them early.
I don’t want my build system to be a scavenger hunt. I want it to be boring—and correct.
pnpm’s storage model: fewer duplicates, more sanity⌗
The headline difference most people miss is not the speed—it’s the storage strategy. pnpm uses content-addressable storage (store once, reuse everywhere) and then hard-links dependencies into each project.
What that means in practice:
- If 20 projects use the same version of
react, you don’t keep 20 physical copies ofreact’s files. - pnpm keeps a global store and links what each project needs.
- On developer machines that run lots of repositories, this is the difference between “my disk is fine” and “why is my laptop suddenly out of space?”
I’ve experienced this firsthand while cleaning up after a long npm era: folders that looked harmless turned into multi-gigabyte node_modules duplicates. pnpm doesn’t eliminate disk usage (dependencies still exist), but it prevents the quiet multiplication that feels unavoidable with npm.
If you manage monorepos, CI caches, or a personal machine with a dozen repos, this becomes immediately tangible. My installs aren’t just faster—they’re less wasteful. That matters because storage pressure is the silent tax that slows everything else down: backups, indexing, container builds, and even editor performance.
Faster installs: not magic, just better mechanics⌗
The second reason I’m migrating is simple: pnpm installs faster for me, and the gap has been consistent enough that I stopped doubting it.
Two mechanisms drive that improvement:
- Reuse from the global store. If the package versions already exist in the store, pnpm can link rather than re-download and re-write everything.
- Deterministic resolution. pnpm knows exactly what should be installed for a project, instead of leaning on whatever npm happens to flatten or hoist.
If you want a practical feel, run this on a clean environment:
- Create two temporary directories: one for npm, one for pnpm.
- Copy the same
package.json/lockfile setup. - Blow away caches as needed (or use a fresh node environment).
- Measure install time.
You don’t have to chase microbenchmarks. The goal is confidence: “Will this be faster every time I start work?” In my experience, yes. And once you’re used to it, npm’s slower churn starts to feel like dragging a chain.
Strict dependency resolution: pnpm catches the “phantoms”⌗
Here’s the part I care about most: pnpm forces dependency truth.
With npm’s permissive hoisting/flattening behavior, you can accidentally rely on packages you never declared. The dependency might appear because another package pulled it in somewhere else in the tree. npm can make that feel “fine” until the dependency graph changes—perhaps after an unrelated upgrade.
pnpm’s strictness changes the incentive structure:
- If your code imports
left-pad, you must declareleft-pad(or the package providing it). - If a tool expects a peer dependency, pnpm pushes you toward the correct setup instead of silently tolerating ambiguity.
- If something is missing, pnpm fails early during install rather than letting it explode later in runtime.
This is not just pedantry. It’s how you prevent “works on my machine” from becoming a lifestyle.
A concrete example: accidental transitive reliance⌗
Suppose you’re building a tool:
import somePlugin from 'some-plugin';
Your package.json includes some-plugin as a dependency, sure. But your code might also implicitly rely on some-plugin pulling in some-utils, and you never declared some-utils yourself.
With npm, some-utils might end up available via hoisting and flattening. With pnpm, you’ll discover the dependency is not actually part of your project contract. You’ll either:
- add
some-utilsexplicitly, or - update how you import/use things so the dependency is truly not needed
That’s the point: pnpm makes dependency boundaries real.
The migration path: painless for most projects (and worth it)⌗
Let’s talk about what “mass-migration” actually looks like in the real world. I’m not suggesting you rewrite your entire ecosystem overnight. The migration for most projects is straightforward:
Pick pnpm and install it
If you’re using Corepack, you can enable it; otherwise install pnpm globally.Generate a lockfile
Runpnpm installto createpnpm-lock.yamlbased on your existingpackage.json. You’ll keep your current semantic intent; you’re just switching the installer and the lock format.Update scripts with confidence
Most scripts (test,build,lint) don’t care whether npm or pnpm runs them. The command runner remains Node-centric. Where differences pop up, it’s usually around assumptions about module layout—which pnpm is intentionally stricter about.Fix the few errors that reveal hidden dependency bugs
The first pnpm install after migration is often an opportunity, not a crisis. If something fails, it’s because the project relied on undeclared transitive dependencies. Add the missing dependency, fix peer dependency declarations, or adjust tooling configuration.Update CI and caching
Cache pnpm’s store rather than cachingnode_modulesblobs. This is a major win for repeat builds and keeps CI artifacts lean.
For monorepos, you’ll also want to standardize how workspaces are configured. The upside is that once pnpm is consistent across repos, the whole fleet starts behaving predictably.
What about edge cases?⌗
The only reason you might struggle is if your project has grown into a “dependency soup” where code imports things it never declared. That’s not a pnpm problem—it’s a hygiene problem waiting to be exposed.
In practice, these fixes are usually small: add a dependency, correct a peer dependency declaration, or update a toolchain package. pnpm doesn’t just move files around; it teaches your project to be honest.
Why inertia is the only reason npm still wins⌗
There’s a reflex in the JavaScript ecosystem: npm is “default,” so it must be the safe choice. But defaults aren’t merit. They’re momentum.
pnpm offers:
- Faster installs driven by reuse and deterministic behavior
- Strict dependency resolution that catches phantom dependencies early
- Disk space savings from global content-addressable storage and hard-linking
- A more predictable developer experience across clean installs and new machines
If your organization values stability and correctness, pnpm is the better contract. If you’re optimizing for “it happens to work today,” npm’s permissiveness can feel convenient—but that convenience comes due later, usually during upgrades or onboarding.
Mass migration isn’t about switching for novelty. It’s about removing a source of accidental complexity that has been slowing you down quietly for years.
Conclusion: make dependency truth and faster installs your default⌗
I migrated because I was tired of invisible fragility: installs that took longer than they should, disks filling with duplicated dependencies, and bugs that only appeared after clean installs. pnpm gave me a better setup with practical benefits I can feel every day: less wasted disk, faster installs, and dependency resolution that forces correctness instead of guessing.
If you have multiple projects—or even a single repo that keeps growing—start the migration. You’ll likely fix a handful of undeclared dependencies, replace guesswork with guarantees, and wonder why you didn’t do it sooner.