JavaScript package managers compared - npm, Yarn, or pnpm?

Three major players exist in the field of package managers today:

  1. npm
  2. Yarn — We will see shortly that Yarn can refer to either Yarn Classic (< v2, or its more modern version Yarn Berry (≥ v2)
  3. performant npm (pnpm)

Originally published at blog.logrocket.com

Virtually, we’ve achieved feature-parity among all package managers, so most likely you’ll decide which package manager to use based on non-functional requirements, like installation speed, storage consumption, or how it meshes with your existing workflow.

Of course, how you choose to use each package manager will differ, but they all share a set of main concepts. You can do the following with any of these package managers:

  • Handle and write metadata
  • Batch install or update all dependencies
  • Add, update, and remove dependencies
  • Run scripts
  • Publish packages
  • Perform security audits

Despite this parity, though, package managers differ under the hood. Traditionally, npm and Yarn have installed dependencies in a flat node_modules folder. But this dependency resolution strategy is not free of criticism.

Thus, pnpm has introduced some new concepts to store dependencies more efficiently in a nested node_modules folder. Yarn Berry goes even further by ditching node_modules completely with its Plug’n’Play (PnP) mode.

In this article, we’ll cover the following things, comparing implementation options where applicable:

  • A brief history of JavaScript package managers
  • Installation workflows
  • Project structures
  • Lock files and dependency storage
  • CLI commands
  • Configuration files
  • Monorepo support
  • Performance and disk-space efficiency
  • Security features
  • Adoption by popular projects

Feel free to skip around and read what’s most relevant to you.

How to use the companion project

I’ve created a companion React app to demonstrate some of the different package managers’ unique concepts. There exists a corresponding Git branch for every package manager variant. This is the project I also used to create the performance table in the below section of this post.

Although the type of application is not important for the topic of this article, I have chosen a mid-size and realistic project to be able to illuminate different aspects; as an example from the recent past, Yarn Berry’s PnP mechanism caused some heated discussions about compatibility issues that this project is suited to help examine.

A brief history of JavaScript package managers

The very first package manager ever released was npm, back in January 2010. It established the core principles of how package managers work today.

If npm has been around for over 10 years, why are there any alternatives at all? Here are some key reasons why that have emerged:

  • Different dependency resolution algorithms with different node_modules folder structures (nested vs. flat, node_modules vs. PnP mode)
  • Different support for hoisting, which has security implications
  • Different lock file formats, each of which have performance implications
  • Different approaches to storing packages on disk, which has implications on disk-space efficiency
  • Different support for multi-package projects (a.k.a., workspaces), which impacts the maintainability and speed of large monorepos
  • Differing needs for new tools and commands, each of which have DX implications
    • Relatedly, different needs for extensibility through plugins and community tools
  • Different degrees of configurability and flexibility

Let’s dive into a brief history of how these needs were identified after npm rose to prominence, how Yarn Classic solved some of them, how pnpm has expanded on these concepts, and how Yarn Berry, as Yarn Classic’s successor, tried to break the mold set by these traditional concepts and processes.

npm, the pioneer

npm is the forefather of package managers. Mistakenly, many people believe npm is an acronym for “Node package manager” but this is not the case. Nevertheless, it’s bundled with the Node.js runtime.

Its release constituted a revolution because, until then, project dependencies were downloaded and managed manually. Concepts like the package.json file with its metadata fields (e.g., devDependencies), storing dependencies in node_modules, custom scripts, public and private package registries, and more, were all introduced by npm.

In 2020, GitHub acquired npm, so in principle, npm is now under the stewardship of Microsoft. At the time of this writing, the most recent major version is v8, released in October 2021.

Yarn (v1 / Classic), responsible for many innovations

In an October 2016 blog post, Facebook announced a collaborative effort with Google and a few others to develop a new package manager that would solve the issues with consistency, security, and performance problems that npm had at the time. They named the alternative Yarn, which stands for Yet Another Resource Negotiator.

Though they based Yarn’s architectural design on many concepts and processes that npm established, Yarn had a major impact on the package manager landscape in its initial release. In contrast to npm, Yarn parallelized operations in order to speed up the installation process, which had been a major pain point for early versions of npm.

Yarn set the bar higher for DX, security, and performance, and also invented many concepts, including:

  • Native monorepo support
  • Cache-aware installs
  • Offline caching
  • Lock files

Yarn v1 entered maintenance mode in 2020. Since then, the v1.x line has been considered legacy and was renamed to Yarn Classic. Its successor, Yarn v2 or Berry, is now the active development branch.

pnpm, fast and disk-efficient

Version 1 of pnpm was released in 2017 by Zoltan Kochan. It is a drop-in replacement for npm, so if you have an npm project, you can use pnpm right away!

The main problem the creators of pnpm had with npm and Yarn was the redundant storage of dependencies that were used across projects. Though Yarn Classic had speed advantages over npm, it used the same dependency resolution approach, which was a no-go for the creators of pnpm: npm and Yarn Classic used hoisting to flatten their node_modules.

Instead of hoisting, pnpm introduced an alternative dependency resolution strategy: content-addressable storage. This method results in a nested node_modules folder that stores packages in a global store on your home folder (~/.pnpm-store/). Every version of a dependency is physically stored in that folder only once, constituting a single source of truth and saving quite a bit of disk space.

This is achieved through a node_modules layout, using symlinks to create a nested structure of dependencies, where every file of every package inside the folder is a hard link to the store. The following diagram from the official documentation clarifies this.

source: https://pnpm.io/motivation#creating-a-non-flat-node_modules-directory

The influence of pnpm can be seen in their 2021 report: competitors want to adopt pnpm’s installation concepts, like the symlinked node_modules structure and the disk-efficient management of packages due to their innovations in content-addressable storage.

Yarn (v2, Berry), reinvents the wheel with Plug’n’Play

Yarn 2 was released in January 2020 and was billed as a major upgrade from the original Yarn. The Yarn team began referring to it as Yarn Berry to make it more obvious that it was essentially a new package manager with a new code base and new principles.

The main innovation of Yarn Berry is its Plug’n’Play (PnP) approach, which came about as a strategy to fix node_modules. Instead of generating node_modules, a .pnp.cjs file with dependency lookup tables is generated, which can be processed more efficiently because it’s a single file instead of a nested folder structure. In addition, every package is stored as a zip file inside of the .yarn/cache/ folder, which takes up less disk space than the node_modules folder.

All of this change, and so quickly, led to a great deal of controversy after release. PnP’s breaking changes required maintainers to update their existing packages in order to be compatible with it. The brand new PnP approach was used by default, and reverting to node_modules was not initially straightforward, which led to many prominent developers openly criticizing Yarn 2 for not making it opt-in.

The Yarn Berry team has since tackled many issues in its subsequent releases. To address the incompatibility of PnP, the team offered some ways to easily change the default operation mode. With the help of a node_modules plugin, just one line of configuration was needed to use the traditional node_modules approach.

Additionally, the JavaScript ecosystem has provided more and more support for PnP over time, as you can see in this compatibility table, and some big projects have moved to adopt Yarn Berry. In my companion project, I was also able to properly implement PnP with my demo React project.

Although Yarn Berry is quite young, it, too, has already an impact on the package manager landscape — pnpm adopted a PnP approach in late 2020.

Installation workflows

A package manager has to be installed on every developer’s local and CI/CD systems first.

npm

npm is shipped with Node.js, so no extra step is needed. Besides downloading the Node.js installer for your OS, it has become common practice to use CLI tools for managing software versions. In the context of Node, Node Version Manager (nvm) or Volta have become very handy utilities.

Yarn Classic and Yarn Berry

You can install Yarn 1 in different ways, e.g., as an npm package with $ npm i -g yarn.

To migrate from Yarn Classic to Yarn Berry, the recommended way is to:

  • Install or update Yarn Classic to the latest 1.x version
  • Use the yarn set version command to upgrade to the latest modern version: $ yarn set version berry

However, the recommended way to install Yarn Berry is via Corepack.

Corepack was created by the folks of Yarn Berry. The initiative was originally named package manager manager (pmm) 🤯 and merged with Node in LTS v16.

With the help of Corepack, you don’t have to install npm’s alternative package managers “separately” because Node includes Yarn Classic, Yarn Berry, and pnpm binaries as shims. These shims allow users to run Yarn and pnpm commands without having to explicitly install them first, and without cluttering the Node distribution.

Corepack comes preinstalled with Node.js ≥ v16.9.0. However, for older Node versions, you can install it using $ npm install -g corepack.

Enable Corepack first, before using it. The example shows how to activate it in Yarn Berry v3.1.1.

# you need to opt-in first
$ corepack enable
# shim installed but concrete version needs to activated
$ corepack prepare yarn@3.1.1 --activate

pnpm

You can install pnpm as an npm package with $ npm i -g pnpm. You can also install pnpm with Corepack: $ corepack prepare pnpm@6.24.2 --activate.

Project structures

In this section, you’ll see the main characteristics of the different package managers at glance. You can easily spot which files are involved in configuring particular package managers, and which files are generated by an installation step.

All package managers store all important meta information in the project manifest file, package.json. Further, a config file at the root level can be used to set up private registries or dependency resolution methods.

With an install step, dependencies are stored in a file structure (e.g., within node_modules) and a lock file is generated. This section does not take a workspaces setup into account, so all examples only show a single location where dependencies are stored.

npm

With $ npm install, or the shorter $ npm i, a package-lock.json file and a node_modules folder are generated. An optional .npmrc config file can be placed at the root level. See the next section for more information on lock files.

    .
    ├── node_modules/
    ├── .npmrc
    ├── package-lock.json
    └── package.json

Yarn Classic

Running $ yarn creates a yarn.lock file and a node_modules folder. A .yarnrc file can be also be a configuration option; Yarn Classic also honors .npmrc files. Optionally, a cache folder (.yarn/cache/) and a location storing the current Yarn Classic version (.yarn/releases/) can be used. Different ways to configure this can be seen in the section comparing configurations.

    .
    ├── .yarn/
    │   ├── cache/
    │   └── releases/
    │       └── yarn-1.22.17.cjs
    ├── node_modules/
    ├── .yarnrc
    ├── package.json
    └── yarn.lock

Yarn Berry with node_modules

Independent of the install mode, you’ll have to handle more files and folders in Yarn Berry projects than projects that use the other package managers. Some are optional and some are mandatory.

Yarn Berry no longer honors .npmrc or .yarnrc files; instead, a .yarnrc.yml config file is required. For a traditional workflow with a generated node_modules folder, you have to provide a nodeLinker config that uses either node_modules or pnpm-inspired installation variant.

    # .yarnrc.yml
    nodeLinker: node-modules # or pnpm

Running $ yarn installs all dependencies in a node_modules folder. A yarn.lock file is generated, which is newer but incompatible with respect to Yarn Classic. In addition, a .yarn/cache/ folder is generated used for offline installs. The releases folder is optional and stores the version of Yarn Berry that is used by the project, as we’ll see in the section comparing configurations.

    .
    ├── .yarn/
    │   ├── cache/
    │   └── releases/
    │       └── yarn-3.1.1.cjs
    ├── node_modules/
    ├── .yarnrc.yml
    ├── package.json
    └── yarn.lock

Yarn Berry with PnP

For both strict and loose PnP modes, executing $ yarn generates .yarn/cache/ and .yarn/unplugged/, along with .pnp.cjs and yarn.lock files. PnP strict is the default mode, but for loose, a config is required.

    # .yarnrc.yml
    nodeLinker: pnp
    pnpMode: loose

In a PnP project, the .yarn/ folder will most likely contain an sdk/ folder to provide IDE support besides a releases/ folder. There are even more folders that can be part of .yarn/, depending on your use case.

    .
    ├── .yarn/
    │   ├── cache/
    │   ├── releases/
    │   │   └── yarn-3.1.1.cjs
    │   ├── sdk/
    │   └── unplugged/
    ├── .pnp.cjs
    ├── .pnp.loader.mjs
    ├── .yarnrc.yml
    ├── package.json
    └── yarn.lock

pnpm

The initial state of a pnpm project looks just like an npm or a Yarn Classic project — you need a package.json file. After installing the dependencies with $ pnpm i, a node_modules folder is generated, but its structure is completely different because of its content-addressable storage approach.

pnpm also generates its own version of a lock file, pnp-lock.yml. You can provide additional configuration with an optional .npmrc file.

    .
    ├── node_modules/
    │   └── .pnpm/
    ├── .npmrc
    ├── package.json
    └── pnpm-lock.yml

Lock files and dependency storage

As described in the previous section, every package manager creates lock files.

Lock files store exactly the versions of each dependency installed for your project, enabling more predictable and deterministic installs. This is required because dependency versions are most likely declared with version ranges (e.g., ≥ v1.2.5) and, therefore, the actually-installed versions could differ if you don’t “lock down” your versions.

Lock files also sometimes store checksums, which we’ll cover in more depth in our section on security.

Lock files have been an npm feature since v5 (package-lock.json), in pnpm from day one (pnpm-lock.yaml), and in a new YAML format in Yarn Berry (yarn.lock).

In the previous section, we saw the traditional approach, where dependencies are installed in a node_modules folder structure. This is the scheme npm, Yarn Classic, and pnpm all use, wherein pnpm does it more efficiently than the others.

Yarn Berry in PnP mode does it differently. Instead of a node_modules folder, dependencies are stored as zip files in combination of a .yarn/cache/ and .pnp.cjs file.

It is best to have these lock files under version control because it solves the “works on my machine” problem — every team member installs the same versions.

CLI commands

The following tables compare a curated set of different CLI commands available in npm, Yarn Classic, Yarn Berry, and pnpm. This is by no means a complete list, but constitutes a cheat sheet. This section does not cover workspace-related commands.

npm and pnpm specially feature many command and option aliases, which means that commands can have different names, i.e., $ npm install is the same as $ npm add. Additionally, many command options have short versions, e.g., -D instead of --save-dev.

In the tables, I’ll refer to all short versions as aliases. With all package managers, you can add, update, or remove multiple dependencies by separating them with spaces (e.g., npm update react react-dom). For the sake of clarity, examples only show usage with single dependencies.

Dependency management

This table covers dependency management commands to install or update all dependencies specified in package.json, or multiple dependencies by specifying them in the commands.

Action npm Yarn Classic Yarn Berry pnpm
install deps in package.json npm install
alias: i, add
yarn install or yarn like Classic pnpm install
alias: i
update deps in package.json acc. semver npm update
alias: up, upgrade
yarn upgrade yarn semver up (via plugin) pnpm update
alias: up
update deps in package.json to latest N/A yarn upgrade latest yarn up pnpm update latest
alias: L
update deps acc. semver npm update react yarn upgrade react yarn semver up react pnpm up react
update deps to latest npm update react@latest yarn upgrade react latest yarn up react pnpm up L react
update deps interactively N/A yarn upgrade-interactive yarn upgrade-interactive (via plugin) $ pnpm up interactive
alias: i
add runtime deps npm i react yarn add react like Classic pnpm add react
add dev deps npm i D babel
alias: savedev
yarn add D babel
alias: dev
like Classic pnpm add D babel
alias: savedev
add deps to package.json without semver npm i E react
alias: saveexact
yarn add E react
alias: exact
like Classic pnpm add E react
alias: saveexact
uninstall deps and remove from package.json npm uninstall react
alias: remove, rm, r, un, unlink
yarn remove react like Classic pnpm remove react
alias: rm, un, uninstall
uninstall deps w/o update of package.json npm uninstall
nosave
N/A N/A N/A

Package execution

The following examples show how to manage packages constituting utility tools during development time — a.k.a., binaries, such as ntl, to interactively execute scripts. The terminology used in the table:

  • package: dependency or binary
  • binary: an executable utility that is executed from node_modules/.bin/ or .yarn/cache/ (PnP)

It is important to understand that Yarn Berry only allows us to execute binaries we’ve specified in our package.json or that are exposed in your meta field for security reasons. pnpm features the same security behavior.

Action npm Yarn Classic Yarn Berry pnpm
install packages globally npm i g ntl
alias: global
yarn global add ntl N/A (global removed) pnpm add global ntl
update packages globally npm update g ntl yarn global upgrade ntl N/A pnpm update global ntl
remove packages globally npm uninstall g ntl yarn global remove ntl N/A pnpm remove
global ntl
run binaries from terminal npm exec ntl yarn ntl yarn ntl pnpm ntl
run binaries from script ntl ntl ntl ntl
dynamic package execution npx ntl N.A. yarn dlx ntl pnpm dlx ntl

Common commands

This table covers useful inbuilt commands. If there is no official command, often a third-party command can be used, via an npm package or Yarn Berry plugin.

  npm Yarn Classic Yarn Berry pnpm
publish package npm publish yarn publish yarn npm publish pnpm publish
list installed deps npm ls
alias: list, la, ll
yarn list   pnpm list
alias: ls
list outdated deps npm outdated yarn outdated yarn upgradeinteractive pnpm outdated
print info about deps npm explain ntl
alias: why
yarn why ntl like Classic pnpm why ntl
init project npm init y
npm init (interactive)
alias: yes
yarn init y
yarn init (interactive)
alias: yes
yarn init pnpm init y
pnpm init (interactive)
alias: yes
print licenses info N/A (via thirdparty package) yarn licenses list N/A (or via plugin, other plugin) N/A (via thirdparty package)
update package manager version N/A (with thirdparty tools, e.g., nvm) yarn policies setversion 1.13.0
with npm
yarn set version 3.1.1
with Corepack
N/A (with npm, Corepack)
perform security audit npm audit yarn audit yarn npm audit pnpm audit

Configuration files

Configuring package managers takes place in both your package.json and dedicated config files. Examples for configuration options are:

  • Define the exact version to use
  • Use a particular dependency resolution strategy
  • Configure access to a private registry
  • Tell the package manager where to find workspaces within a monorepo

npm

Most configuration takes place in a dedicated config file (.npmrc).

If you want to use npm’s workspaces feature, you have to add a configuration to the package.json by using the workspaces metadata field to tell npm where to find the folders constituting sub-projects or workspaces, respectively.

    {
      // ...
      "workspaces": [
        "hooks",
        "utils"
      ]
    }

Every package manager works out of the box with the public npm registry. In a company context with shared libraries, you’ll most likely want to reuse them without publishing them to a public registry. To config a private registry, you can do this in a .npmrc file.

    # .npmrc
    @doppelmutzi:registry=https://gitlab.doppelmutzi.com/api/v4/projects/41/packages/npm/

There exist many configuration options for npm, and they are best viewed in the docs.

Yarn Classic

You can setup Yarn workspaces in your package.json. It is analogous to npm, but the workspace has to be a private package.

    {
      // ...
      "private": true,
      "workspaces": ["workspace-a", "workspace-b"]
    }

Any optional configurations go into a .yarnrc file. A common configuration option is to set a yarn-path, which enforces a particular binary version to be used by every team member. The yarn-path directs to a folder (e.g., .yarn/releases/) containing a particular Yarn version. You can install a Yarn Classic version with the yarn policies command.

Yarn Berry

Configuring workspaces in Yarn Berry is also analogous to how it’s done in Yarn Classic, with a package.json. Most Yarn Berry configuration takes place in .yarnrc.yml, and there are many configuration options available. The Yarn Classic example is also possible, but the metadata field is renamed to yarnPath.

    # .yarnrc.yml
    yarnPath: .yarn/releases/yarn-3.1.1.cjs

Yarn Berry can be extended with plugins by using the [yarn plugin import](https://yarnpkg.com/cli/plugin/import). This command updates the .yarnrc.yml.

    # .yarnrc.yml
    plugins:
      - path: .yarn/plugins/@yarnpkg/plugin-semver-up.cjs
        spec: "https://raw.githubusercontent.com/tophat/yarn-plugin-semver-up/master/bundles/%40yarnpkg/plugin-semver-up.js"

As described in the history section, there might be problems with dependencies in PnP strict mode due to incompatibility. There is a typical solution for such a PnP problem: the packageExtensions configuration property. You can follow the next example with the companion project.

    # .yarnrc.yml
    packageExtensions:
      "styled-components@*":
        dependencies:
          react-is: "*"

pnpm

pnpm uses the same configuration mechanism as npm, so you can use a .npmrc file. Configuring a private registry also works the same way as with npm.

With pnpm’s workspaces feature, support for multi-package projects is available. To initialize a monorepo, you have to specify the location of the packages in a pnpm-workspace.yaml file.

    # pnpm-workspace.yaml
    packages:
      - 'packages/**'

Monorepo support

What is a monorepo?

A monorepo is a repository that houses multiple projects, which are referred to as workspaces or packages. It is a project organization strategy to keep everything in one place instead of using multiple repositories.

Of course, this comes with additional complexity. Yarn Classic was the first to enable this functionality, but now every major package manager offers a workspaces feature. This section shows how to configure workspaces with each of the different package managers.

npm

The npm team released the long-awaited npm workspaces feature in v7. It contained a number of CLI commands that helped manage multi-package projects from within a root package. Most of the commands can be used with workspace-related options to tell npm if it should run against a specific, multiple, or all workspaces.

    # Installing all dependencies for all workspaces
    $ npm i --workspaces.
    # run against one package
    $ npm run test --workspace=hooks
    # run against multiple packages
    $ npm run test --workspace=hooks --workspace=utils
    # run against all
    $ npm run test --workspaces
    # ignore all packages missing test
    $ npm run test --workspaces --if-present

In contrast to the other package managers, npm v8 doesn’t currently support advanced filtering or executing multiple workspace-related commands in parallel.

Yarn Classic

In August 2017, the Yarn team announced first-class monorepo support in terms of a workspaces feature. Prior to this point, it was only possible to use a package manager in a multi-package project with third-party software like Lerna. This addition to Yarn paved the way for other package managers to implement such a feature, too.

I’ve also written previously about how to use Yarn Classic’s workspaces feature with and without Lerna, if you’re interested. But this post will only cover some necessary commands to help you manage dependencies in a Yarn Classic workspaces setup.

    # Installing all dependencies for all workspaces
    $ yarn
    # display dependency tree
    $ yarn workspaces info
    # run start command only for one package
    $ yarn workspace awesome-package start
    # add Webpack to package
    $ yarn workspace awesome-package add -D webpack
    # add React to all packages
    $ yarn add react -W

Yarn Berry

Yarn Berry featured workspaces from the beginning because its implementation was built upon Yarn Classic’s concepts. In a Reddit comment, a main developer of Yarn Berry gave a brief overview of workspace-oriented features, including:

Yarn Berry makes heavy use of protocols, which can be used in either the dependencies or devDependencies fields of package.json files. One of them is the workspace: protocol.

In contrast to Yarn Classic’s workspaces, Yarn Berry explicitly defines that a dependency has to be one of the packages in this monorepo. Otherwise, Yarn Berry might try to fetch a version from a remote registry if the versions do not match.

    {
      // ...
      "dependencies": {
        "@doppelmutzi/hooks": "workspace:*",
        "http-server": "14.0.0",
        // ...
      }  
    }

pnpm

With its workspace: protocol, pnpm facilitates monorepo projects similarly to Yarn Berry. Many pnpm commands accept options like --recursive (-r) or –filter that are especially useful in a monorepo context. Its native filtering command is also a good supplement or replacement for Lerna.

    # prune all workspaces  
    pnpm -r exec -- rm -rf node_modules && rm pnpm-lock.yaml  
    # run all tests for all workspaces with scope @doppelmutzi
    pnpm recursive run test --filter @doppelmutzi/

Performance and disk-space efficiency

Performance is a crucial part of decision-making. This section shows my benchmarks based on one small and one medium-sized project. Here are some notes about the sample projects:

  • Neither set of benchmarks uses workspace features
  • The small project specifies 33 dependencies
  • The medium project specifies 44 dependencies

I performed measurements for three use cases (UC), once for each of our package manager variants. To find out about the detailed evaluation with explanations, take a look at the results for project 1 (P1) and project 2 (P2).

  • UC 1: No cache/store, no lock files, no node_modules or .pnp.cjs
  • UC 2: cache/store exists, no lock files, no node_modules or .pnp.cjs
  • UC 3: cache/store exists, lock files exist, no node_modules or .pnp.cjs

I used the tool gnomon to measure the time an install consumes (e.g., $ yarn | gnomon). In addition, I measured the sizes of generated files, e.g., $ du -sh node_modules.

With my projects and my measurements, Yarn Berry PnP strict was the winner in terms of installation speed for all use cases and both projects.

Performance results for Project 1

Method npm
v8.1.2
Yarn Classic
v1.23.0
pnpm
v6.24.4
Berry PnP loose
v3.1.1
Berry PnP
v3.1.1
Berry nm
v3.1.1
Berry
pnpm
v3.1.1
UC 1 86.63s 108.89s 43.58s 31.77s 30.13s 56.64s 60.91s
UC 2 41.54s 65.49s 26.43s 12.46s 12.66s 46.36s 40.74s
UC 3 23.59s 40.35s 20.32s 1.61s 1.36s 28.72s 31.89s
  package-lock.json: 1.3M node_modules: 467M node_modules: 397M
yarn.lock: 504K
pnpm-lock.yaml: 412K
node_modules: 319M
yarn.lock: 540K
cache: 68M
unplugged: 29M
.pnp.cjs: 1.6M
yarn.lock: 540K
cache: 68M
unplugged: 29M
.pnp.cjs: 1.5M
node_modules: 395M
yarn.lock: 540K
cache: 68M
node_modules: 374M
yarn.lock: 540K
cache: 68M

Performance results for Project 2

Method npm
v8.1.2
Yarn Classic v1.23.0 pnpm
v6.24.4
Berry PnP loose
v3.1.1
Berry PnP
v3.1.1
Berry nm
v3.1.1
Berry
pnpm
v3.1.1
UC 1 34.91s 43.26s 15.6s 13.92s 6.44s 23.62s 20.09s
UC 2 7.92s 33.65s 8.86s 7.09s 5.63s 15.12s 14.93s
UC 3 5.09s 15.64s 4.73s 0.93s 0.79s 8.18s 6.02s
  package-lock.json: 684K
node_modules: 151M
yarn.lock: 268K
node_modules: 159M
pnpm-lock.yaml: 212K
node_modules: 141M
.pnp.cjs: 1.1M
yarn.lock: 292K
.yarn: 38M
.pnp.cjs: 1.0M
yarn.lock: 292K
.yarn: 38M
yarn.lock: 292K
node_modules: 164M
cache: 34M
yarn.lock: 292K
node_modules: 156M
cache: 34M

Here are the official benchmarks of the Yarn Berry team and of pnpm.

Security features

npm

npm has been a bit too forgiving when it comes to working with bad packages, and has experienced some security vulnerabilities that directly affected many projects. For example, in version 5.7.0, when you executed the sudo npm command on a Linux OS, it became possible to change the ownership of system files, rendering the OS unusable.

Another incident occurred in 2018 and involved the theft of Bitcoin. Basically, the popular Node.js package EventStream added a malicious dependency in its version 3.3.6. This malicious package contained an encrypted payload that tried to steal Bitcoin from the developer’s machine.

To help solve these issues, more recent npm versions use the SHA-512 cryptography algorithm in the package-lock.json to check the integrity of the packages you install.

Overall, npm has done more and more to close their security gaps, especially those made more obvious when compared to Yarn.

Yarn

Both Yarn Classic and Yarn Berry have verified the integrity of each package with checksums stored in yarn.lock since the beginning. Yarn also tries to prevent you from retrieving malicious packages that are not declared in your package.json during installation: if a mismatch is found, the installation is aborted.

Yarn Berry in PnP mode does not suffer from the security problems of the traditional node_modules approach. In contrast to Yarn Classic, Yarn Berry improves the security of command execution. You can only execute binaries of dependencies that you have explicitly declared in your package.json. This security feature is similar to pnpm, which I’ll describe next.

pnpm

pnpm also uses checksums to verify the integrity of every installed package before its code is executed.

As we alluded to above, npm and Yarn Classic each have security issues due to hoisting. pnpm avoids this because its model doesn’t use hoisting; instead, it generates nested node_modules folders that remove the risk of illegal dependency access. This means that dependencies can only access other dependencies if they are explicitly declared in package.json.

This is especially crucial in a monorepo setup, as we discussed, because the hoisting algorithm can sometimes lead to phantom dependencies and doppelgangers.

I analyzed many popular open source projects to get an idea of which package managers are used nowadays by the “developer elite.” It was important for me that these projects are actively maintained and last updated recently. This might give you another perspective when choosing a package manager.

npm Yarn Classic Yarn Berry pnpm
Svelte React Jest (with nm) Vue 3
Preact Angular Storybook (with nm) Browserlist
Express.js Ember Babel (with nm) Prisma
Meteor Next.js Redux Toolkit (with nm) SvelteKit
Apollo Server Gatsby    
  Nuxt    
  Create React App    
  webpack-cli    
  Emotion    

Interestingly, at the time of this writing, none of these open-source projects uses a PnP approach.

Conclusion

The current state of package managers is great. We have virtually attained feature parity among all major package managers. But still, they do differ under the hood quite a bit.

pnpm looks like npm at first because their CLI usage is similar, but managing dependencies is much different; pnpm’s method leads to better performance and the best disk-space efficiency. Yarn Classic is still very popular, but it’s considered legacy software and support might be dropped in the near future. Yarn Berry PnP is the new kid on the block, but hasn’t fully realized its potential to revolutionize the package manager landscape once again.

Over the years, many users have asked about who uses which package managers, and overall, it seems folks are especially interested in the maturity and adoption of Yarn Berry PnP.

The goal of this article is to give you many perspectives to make a decision about which package manager to use on your own. I would like to point out that I do not recommend a particular package manager. It depends on how you weight different requirements — so you can still choose whatever you like!

Written on April 17, 2022