DEV Community

Philipp Rich
Philipp Rich

Posted on

Why I Switched to pnpm for Node.js Development

dev.to doesn't support mermaid diagrams. You can read article with diagrams on my blog

Introduction

Somewhere on my developer's path, I found myself having a zoo of node versions, package managers and tools, like nvm, npm, pnpm, yarn, corepack just to name a few. And the issue was, that I had no idea how they are related to each other and what are the differences between npm, pnpm and yarn. I dived deeper into this topic and ended up with opionated takes on how to manage node / js development environments.

Use pnpm instead of npm or yarn

npm is built-in package manager, that comes with a node out-of-the-box. Most of "quick starts" mention npm install <package> and it is the first choice for beginner developers. However, it has fundamental flaws and zero reasons to be used as your default package manager.

The major issue of npm is its tremendous hunger for disk space. Imagine a Margaret, full-stack engineer, working with JavaScript. She has 30+ assorted projects on
her laptop: there might be few pet-projects, experiments and couple commercial projects. The average size of project's dependencies in node_modules is 500MB,
so the dependencies alone will take roughly 15GB of space. And npm don't give a shit. It just keeps storing each dependency inside each of the projects. But, what if I tell you, that 60% of Margaret's projects use React, Zod, Tanstack Query and other popular packages? That means, that there are about 16+ copies of React, Zod and other packages, which are used across multiple projects...

And this is one of the strongest argument to use pnpm instead: it keeps all dependencies in a shared store and there will be only a single copy of package, which will be
reused by multiple projects via symlinks/hardlinks or APFS Reflinks(on mac).

By default, when you run pnpm i it first looks is there a specific version of package already in global store and downloads package only if it's not there yet.

To enforce this behavior, you may use following commands:
pnpm i --prefer-offline It first looks at already installed dependencies of any version and if there aren't any, downloads dependency
pnpm i --offline throws error if there is no package available locally

pnpm config set prefer-offline true --global set prefer-offline mode globally and permanently
pnpm config get prefer-offline check if prefer-offline is set globally
pnpm install --no-prefer-offline overwrite the default behavior (if prefer-offline set)
pnpm store prune will remove packages from store, which are not used by any of your projects

pnpm offers another quality-of-life enhancements, such as pnpm i --latest --interactive,
which will simplify upgrading your project's dependencies.

After switching to pnpm I free 20GB of space by deduplicating dependencies among my projects.

But what about Yarn?

Yarn package manager started to use similar approach by storing dependencies globally, but, there is no reason to use yarn over pnpm, because Yarn's PnP will make your life more complicated without providing any real benefits.

However, yarn has a killer feature for specific situations, which ironically switched off by default (IDK why in the world they decided to mimic pnpm by default, instead of highlighting its strong sides). This feature is zero installs.

With zero installs yarn acts more like npm, keeping copies of dependencies in projects. But goes even further by putting them into .zip and publishing to project's git (yeah, no .gitignore for node_modules). What's the point? This approach leads to an enormous boost in performance for ci/cd pipelines, because it simply
excludes yarn install step.

With that said, sometimes Yarn might be the weapon of choice, but not with default settings.

Avoid nvm, fnm and other node version managers

Similar to what npm does to package-level dependencies, the nvm does for global dependencies:

nvm keeps copies of global tools per each node version polluting your disk space.

Single tool to rule them all

How to deduplicate global tools? With pnpm. Yes, it can be your node version manager, excluding the need of extra tooling and eliminating duplication of global tools per node version.

pnpm env add --global lts 16 20.0.1 - installs multiple Node versions
pnpm env use --global 16 - switches to Node 16
pnpm env list - shows installed Node versions

How to specify package manager and node version in a project?

Here things become a bit messy. There are multiple methods for pinning versions of Node and package managers out there. And each of them has different
compatibility.

Simply put, we can use engines, devEngines to inform about Node and PM versions, while npx only-allow <package manager> enforces use of specific package manager.

It's common not to include some of properties, but semantically engines used to inform the consumer of a package (ci/cd pipelines or some integrations) which versions required to run the package, while devEngines inform developers, what versions needed to develop the package. Additionally with devEngines you can specify how package manager should behave if there is no specified version available. In other words, it can mimic
corepack's behavior.

// package.json
{
  "engines": { // For running package (warning only, non-blocking)
    "pnpm": ">=8",
    "node": ">=16"
  },
  "devEngines": { // for develop the project (often omitted) Supported by pnpm 10.14.0+, npm 10.9.0+. Not supported by Yarn
    "runtime": {
      "name": "node",
      "version": "22.14.0",
      "onFail": "download"
    },
    "packageManager": {
      "name": "pnpm",
      "version": ">=9.0.0",
      "onFail": "error"
    },
  "packageManager": "pnpm@10.33.2", // only for corepack
  "scripts": {
    "preinstall": "npx only-allow pnpm" // prevent using other package manager
  },
}
Enter fullscreen mode Exit fullscreen mode

Properties, like engines do not enforce strictness about Node versions meaning they only show warning, not preventing running package on another Node versions. To make this strict, you need to add .npmrc to the project's root:

engine-strict=true
Enter fullscreen mode Exit fullscreen mode

How many disk space is wasted?

While pnpm is saving disk space it is very hard to see the actual size of node_modules on mac. Because pnpm uses APFS Reflinks which reports the size of referenced files instead of actual nearly 0KB size of links. While cleaning my packages myself, I made a tiny cli tool, which shows which of your node/js projects consuming more space. Learn more on github

References

  1. Yarn cache strategies
  2. pnpm documentation - Store
  3. pnpm vs npm vs yarn - Node Package Managers Comparison
  4. Corepack documentation
  5. npx only-allow
  6. npm engines documentation
  7. Why pnpm uses hard links
  8. Managing Node.js versions with pnpm

Happy coding!

Top comments (0)