Node Package Managers in 2022

So many NodeJS package managers! How do you pick one?

One of the things I love about the JavaScript ecosystem is the amount of options we have to solve basically every problem. We get to pick exactly what tools we use and there usually isn't one right way to solve a problem.

However, that does mean you need an informed opinion about most of your tooling options, which can be be a daunting task.

Today, we're going to look at the prominent three Node package managers. By the end of this, you'll have a better understanding of each one and maybe an easier time picking one for your next project.

What is a package manager? #

Simply put, a package manager is software that handles the installation and management of third party packages. Beyond that, package managers also help manage shared code in monorepos and allow developers to create easy-to-access commands in their projects.

Package managers have an interesting place in the project structure. They can affect installation time (and thus build time), but are largely transient outside of those important activities. You'll only really interact with them when you're installing new dependencies or running one of those commands mentioned previously.

However, in a world of CICD pipelines, we want builds to be a quick as possible so that we can save at least a little money and time. Picking the right package manager for your project can save you time, money, and headache.

The Package Managers #

Today we'll be looking at three package managers: npm, Yarn, and pnpm.

All of the package managers we'll be talking about have rough parity. We won't be looking at features like workspaces, which all three of these share. Instead, we're looking at how these package managers do their base job – manage your packages. This will mainly focus on where and how packages are installed.

npm #

npm is the default package manager bundled with the Node.js runtime. Actually, npm is two things - a package manager and a package registry. Even if you switch to a different package manager, you're likely running through the npm package registry for your packages.

Since npm comes with Node.js by default, you already have access to it as soon as you install Node.js, saving you a little time on the installation of another package manager.

How It Works #

npm uses package-lock.json to describe the exact node_modules tree generated by installing, modifying, and removing packages.

npm uses a flattened dependency tree for installing packages. In your local development environment, your node_modules directory generated by npm install will look like the following:

node_modules
  package_1           // depends on package_3 v1
  package_2           // depends on package_3 v2
    node_modules
      package_3 (v2)  // package_2 uses v2 of package_3
  package_3 (v1)      // package_1 uses v1 of package_3

You might notice something interesting here. package_3 is installed twice. This is because both package_1 and package_2 depend on it, but they require different versions. Which one is placed in the top level depends on the order of operations. Since package_1 was installed first, package_2 has an internal node_modules which includes package_3.

npm also uses a cache to store your dependency installs. When you're installing a dependency, npm first looks in your cache for that package. If it finds it, it verifies the integrity of the package before installing it locally. If the package isn't found or has been otherwise corrupted, npm automatically installs the package from the appropriate registry.

Conclusion #

npm's biggest boon is that it's ready to go as soon as you install Node.js. It was the first of the Node package managers and is used everywhere as a result. npm is a solid choice if you're building something small and quick or if you're unable to install other package managers on your build servers in CI.

Fun fact: npm DOESN'T stand of "node package manager" but rather "npm is not an acronym."

Yarn #

Yarn was released in 2016 as an alternative to npm. It introduced a lot of ideas that have since made their way into all of the other package managers, namely lock files, workspaces (IE: monorepo support), and cache-aware installs.

There are two major versions of Yarn in use today: Yarn v1 or Classic, and Yarn Berry. They work pretty differently, but Yarn Classic has been in maintenance mode since 2020, so we recommend going for Yarn Berry.

The recommended way of installing Yarn is to enable Corepack which comes bundled with Node.js in version 16.10 and later. It's as simple as running corepack enable after installing Node.js. You might need to update Yarn to the latest version with yarn set version stable.

How It Works #

Yarn has two different ways of managing local dependencies - a traditional flattened tree and a "zero-install" version utilizing Yarn's Plug'n'Play. The flattened tree is essentially identical to npm's flattened tree. Zero-install installs .zip versions of your dependencies in a .yarn/cache directory and creates a .pnp.cjs file to help your project access those files. You'll check both the cache directory and the .pnp.cjs file into your repository.

It might seem weird to check in your .yarn/cache directory, which is essentially a node_modules directory, but it's actually quite small and appropriate for source control unlike the bulky node_modules directory. There's also a security risk if you cannot trust other developers, such as in an open-source repository. In such a case, you can use yarn install --check-cache in your CI on untrusted PRs to re-download your cache directory and check for any mismatching checksums.

You can always swap out of zero-install by adding .yarn/cache and .pnp.cjs to your .gitignore.

A Word on the Yarn Cache

Since you're checking files into the ./yarn/cache, it can get a little big. They're zip files, so they should be fairly small, but if you have a lot of them, they're going to add up. It's not a bad idea to regularly clean up your cache, especially after updating packages.

When you're pulling down your repo, you can also use git clone <repo> --filter blob:limit=200k and have Git lazy load extra packages. You can read more about this on the Yarn Berry's Github Repo.

Conclusion #

Yarn's Plug'n'Play and zero-install mode are compelling reasons to choose Yarn. Checking your installed packages into your repository ensures that each developer has the exact same package. If this ever becomes a problem, it's as easy as commenting out a few lines in a .gitignore to a more traditional installation method.

Yarn has traditionally been a bit faster than npm, but by their own admission, speed is not Yarn's priority.

PNPM #

pnpm was first released in 2017. It's primary goal is to be fast and efficient.

You can install pnpm a bunch of different ways. Check out their installation page and find your favorite!

How It Works #

pnpm uses pnpm-lock.yaml as its lockfile, but the way pnpm installs and manages packages is a bit different than npm and Yarn. Instead of using the flattened tree, it uses symlinks to reduce the number of installations of a dependency and ensure that your application only has access to the packages you installed.

If you were using npm and simply ran npm install express, your node_modules would look something like this:

node_modules
  accepts
  array-flatten
  body-parser
  bytes
  content-disposition
  cookie-signature
  cookie
  debug
  depd
  destroy
  ee-first
  encodeurl
  escape-html
  etag
  express  // Here's the actual package we wanted
  ...

With pnpm, your install looks like this:

node_modules
  .pnpm
  express  // This links into /.pnpm/express@<version>/node_modules/express
.modules.yaml

This might seem wild. But it ensures that your application only has access to installed packages, and makes dependencies easy to trace and reuse.

Conclusion #

pnpm says they are about 2x as fast as npm and Yarn, but their biggest boon lies in the data efficiency. Symlinking allows pnpm to reduce the number of copy-pasted files in your dependency tree. This can be a really nice boon when space is an issue such as bigger applications being built in CI.

Which to Choose #

We're finally at the end. You've made it this far and you want me to answer the question... which package manager should you use?

Here are some benchmarks for the various package managers we've discussed.

pnpm tends to edge out Yarn and npm on the speed side, but it's not as cut and dry as "pnpm is faster". Furthermore, pnpm's use of symlinking could cause some issues, especially if your team uses a mix of Windows, Mac, and Linux.

I think it ultimately comes down to the following:

  • If you're building something small, use npm.
  • If you're building something big and complex, use pnpm.
  • If you can't use pnpm, use Yarn.
  • Your hosting or CICD solutions might be opinionated on which of these you can use. Check what they support before committing.

Honorable Mention: Bun #

Bun is not just a package manager. It's an entire JavaScript runtime, more closely related to Node and Deno than npm or Yarn. Much like Node.js including npm, Bun includes its own package manager. It just so happens that it is its own package manager.

Bun touts to be 4x to 80x faster than npm! Like npm, it uses a flattened tree approach and carries a bun.lockb lockfile. One of the neat things is that this lockfile is in binary, which allows it to be super speedy compared to yaml and json lockfiles.

Though Bun is not quite ready for the prime time yet, it'll be a strong contender in the coming years and is definitely worth keeping an eye on.

Nick Telsan

Nick is a Developer, working in our Chattanooga, TN office. He has a passion for building things and is never one to shy away from learning new things.

More articles by Nick

Related Articles