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.