{"id":336,"date":"2026-06-17T13:01:00","date_gmt":"2026-06-17T13:01:00","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/pnpm-vs-npm-the-day-node-modules-stopped-eating-my-disk\/"},"modified":"2026-06-17T13:01:00","modified_gmt":"2026-06-17T13:01:00","slug":"pnpm-vs-npm-the-day-node-modules-stopped-eating-my-disk","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/pnpm-vs-npm-the-day-node-modules-stopped-eating-my-disk\/","title":{"rendered":"pnpm vs npm: The Day My node_modules Stopped Eating My Disk"},"content":{"rendered":"<p>My laptop yelled at me about disk space on a Tuesday, which is how most of my infrastructure decisions seem to start. I had something like 14 gigs of <code>node_modules<\/code> scattered across nine project folders, and most of that was the same packages copied over and over, because npm hands every project its own private hoard. I&rsquo;d known about pnpm for years. I nodded along whenever someone brought it up. I just never bothered, since npm works and swapping package managers to reclaim disk felt like exactly the kind of yak-shaving I tease other people for.<\/p>\n<p>Then I actually switched. It took an afternoon, most of which was me squinting at it suspiciously. Six months later I run pnpm on everything and going back would genuinely annoy me. Here&rsquo;s what changed, where it bit me, and the one spot where I still reach for plain npm.<\/p>\n<h2 id=\"what-npm-actually-does-with-your-disk\">What npm actually does with your disk<\/h2>\n<p>When you run <code>npm install<\/code>, npm builds a <code>node_modules<\/code> folder for that one project and fills it with a full copy of every package in your dependency tree. Do that across a handful of repos and you&rsquo;ve got the same version of the same library sitting on your disk five times over.<\/p>\n<pre><code class=\"language-bash\"># every project gets its own complete copy\n~\/code\/app-a\/node_modules   2.1G\n~\/code\/app-b\/node_modules   1.9G\n~\/code\/app-c\/node_modules   2.3G\n<\/code><\/pre>\n<p>None of those copies know the others exist. React, your bundler, your test runner: duplicated per project. On a single app you never notice. Once you&rsquo;re juggling a few services and some side projects, it adds up fast, and the numbers get silly. The pnpm docs lay out the reasoning behind a different approach in their <a href=\"https:\/\/pnpm.io\/motivation\" rel=\"nofollow noopener\" target=\"_blank\">motivation page<\/a>, and it was the first thing that made me stop and reconsider my habit.<\/p>\n<h2 id=\"how-pnpm-stores-things-once\">How pnpm stores things once<\/h2>\n<p>pnpm keeps a single content-addressable store on your machine, usually under <code>~\/.local\/share\/pnpm\/store<\/code>. Every version of every package you&rsquo;ve ever installed lives there exactly once. When a project needs a package, pnpm doesn&rsquo;t copy it. It hard-links the file from the store into that project&rsquo;s <code>node_modules<\/code>.<\/p>\n<pre><code class=\"language-bash\">pnpm install\n# files live once in the global store\n# node_modules becomes a tree of links pointing into that store\n<\/code><\/pre>\n<p>The practical result is that installing a package you already have somewhere else costs almost no extra disk. My nine-project mess dropped from roughly 14 gigs to under 3. The first time I ran <code>du -sh<\/code> afterward I assumed the install had failed.<\/p>\n<p>There&rsquo;s a deeper structural difference too. npm flattens everything into the top level of <code>node_modules<\/code>. pnpm builds a nested layout where each package only sees the dependencies it actually declared. That sounds like a detail. It turned out to be the part I cared about most.<\/p>\n<h2 id=\"the-strictness-i-didnt-know-i-wanted\">The strictness I didn&rsquo;t know I wanted<\/h2>\n<p>Because npm flattens everything, your code can import packages you never asked for. If some library in your tree depends on <code>lodash<\/code>, npm hoists <code>lodash<\/code> to the top, and now your own code can <code>import<\/code> it even though it&rsquo;s nowhere in your <code>package.json<\/code>. People call these phantom dependencies, and they&rsquo;re a quiet time bomb.<\/p>\n<pre><code class=\"language-js\">\/\/ worked fine under npm purely by accident:\n\/\/ lodash was a sub-dependency of something else\nimport _ from 'lodash'   \/\/ never declared in my package.json\n<\/code><\/pre>\n<p>That code runs until the day the library that pulled in <code>lodash<\/code> drops it, and then your build breaks for a reason that has nothing to do with anything you changed. pnpm refuses to play along. If you didn&rsquo;t declare it, you can&rsquo;t import it.<\/p>\n<pre><code class=\"language-bash\"># pnpm error, the good kind\nCannot find module 'lodash'\n# fix is honest:\npnpm add lodash\n<\/code><\/pre>\n<p>The first afternoon, this felt like pnpm being difficult. Three broken imports later I understood it was telling me the truth about what my project depended on. I&rsquo;d been lying to myself with npm and calling it convenience.<\/p>\n<h2 id=\"speed-and-whether-it-actually-matters\">Speed, and whether it actually matters<\/h2>\n<p>pnpm is usually faster than npm, especially on repeat installs, because linking from a warm store beats downloading and copying. The pnpm team keeps <a href=\"https:\/\/pnpm.io\/benchmarks\" rel=\"nofollow noopener\" target=\"_blank\">public benchmarks<\/a> if you want the charts. I&rsquo;ll be honest about my own experience though: a cold install on a fresh machine isn&rsquo;t magic, you still pay to fetch packages the first time. The win shows up in CI and in the tenth install of the day, not the first.<\/p>\n<p>In CI the trick is caching the store rather than <code>node_modules<\/code>. Cache <code>~\/.local\/share\/pnpm\/store<\/code>, and every job after the first reuses it. My pipeline install step went from a minute and change to about fifteen seconds once the cache warmed up. That compounds when you&rsquo;re pushing twenty times a day.<\/p>\n<h2 id=\"workspaces-without-the-footguns\">Workspaces without the footguns<\/h2>\n<p>Monorepos are where pnpm stopped being a nice-to-have and became the reason I won&rsquo;t switch back. You define your packages in one file:<\/p>\n<pre><code class=\"language-yaml\"># pnpm-workspace.yaml\npackages:\n  - &quot;apps\/*&quot;\n  - &quot;packages\/*&quot;\n<\/code><\/pre>\n<p>Internal packages link to each other through the store, so a shared <code>ui<\/code> package is available to every app without publishing anything or copying files around. The strictness from earlier matters even more here, since it stops one app from accidentally leaning on another app&rsquo;s dependencies. I wrote about how I decide between one repo and many in my post on <a href=\"https:\/\/abrarqasim.com\/blog\/monorepo-vs-polyrepo-2026\" rel=\"noopener\">monorepo versus polyrepo<\/a>, and pnpm is doing most of the quiet work that makes the monorepo side livable. The official <a href=\"https:\/\/pnpm.io\/workspaces\" rel=\"nofollow noopener\" target=\"_blank\">workspace docs<\/a> cover the filtering commands, which are the part you&rsquo;ll actually use day to day.<\/p>\n<h2 id=\"where-i-still-reach-for-npm\">Where I still reach for npm<\/h2>\n<p>I&rsquo;m not a zealot about this. A fair amount of tooling still assumes npm in its setup instructions, and some Docker base images and CI templates ship with npm wired in. For a throwaway script or a quick <code>npx<\/code> one-off, I don&rsquo;t bother setting up pnpm. And if I&rsquo;m contributing to a project that already standardized on npm, I use what the team uses, because fighting someone&rsquo;s lockfile is a bad way to say hello. I help teams sort out exactly these kinds of build and tooling decisions in my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">consulting work<\/a>, and the answer is almost never &ldquo;rip everything out on day one.&rdquo;<\/p>\n<p>The point isn&rsquo;t that npm is bad. It&rsquo;s that pnpm&rsquo;s defaults happen to match how I actually want a package manager to behave: store things once, tell me the truth about my dependencies, and get out of the way.<\/p>\n<h2 id=\"what-to-try-this-week\">What to try this week<\/h2>\n<p>Pick one project you don&rsquo;t care about breaking. Delete its <code>node_modules<\/code> and lockfile, run <code>pnpm import<\/code> to convert the existing <code>package-lock.json<\/code>, then <code>pnpm install<\/code>. Try to build and run it. If something can&rsquo;t find a module, that&rsquo;s a phantom dependency you&rsquo;ve been carrying, and now you know. Spend ten minutes adding the real declarations, commit, and see how your <code>node_modules<\/code> size looks afterward. If you like what you see, point pnpm at a second project. That&rsquo;s the whole on-ramp.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I moved from npm to pnpm six months ago and my node_modules went from 14GB to under 3GB. Here&#8217;s what changed, where it bit me, and when I still use npm.<\/p>\n","protected":false},"author":2,"featured_media":335,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I moved from npm to pnpm six months ago and my node_modules went from 14GB to under 3GB. Here's what changed, where it bit me, and when I still use npm.","rank_math_focus_keyword":"pnpm vs npm","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[165,375],"tags":[44,77,379,377,378,376],"class_list":["post-336","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-javascript","category-tooling","tag-javascript","tag-monorepo","tag-node-js","tag-npm","tag-package-manager","tag-pnpm"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/336","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=336"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/336\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/335"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=336"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=336"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=336"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}