Author avatar

Esteban Herrera

Yarn: A package manager for Node.js

Esteban Herrera

  • Jul 23, 2017
  • 17 Min read
  • 23,157 Views
  • Jul 23, 2017
  • 17 Min read
  • 23,157 Views
Node.js

Package Managers

Ruby has Bundler.

PHP has Composer.

Rust has Cargo.

Python has pip.

These package managers automate the process of installing and managing libraries and dependencies.

In Node.js, we have npm as the default package manager, which is automatically included when Node.js is installed.

Npm is not perfect, and there are a number of open source alternatives created to solve some of its issues like ied, pnpm, and more recently, yarn

Introducing Yarn

Yarn was released by Facebook in collaboration with Exponent, Google, and Tilde in October 2016.

There's a post on Facebook Code that describe the reasons for building this new package manager.

Here are some highlights:

... as we scaled internally, we faced problems with consistency when installing dependencies across different machines and users, the amount of time it took to pull dependencies in, and had some security concerns with the way the npm client executes code from some of those dependencies automatically.

We also had to work around issues with npm's shrinkwrap feature, which we used to lock down dependency versions.

... updating a single dependency with npm also updates many unrelated ones based on semantic versioning rules.

Many advantages of using Yarn aren't common knowledge yet. This guide will explore the advantages of Yarn over npm and will cover some basic commands that will help new users settle into using Yarn regularly.

Here's Yarn's Github page. It's a pretty popular project; at the time of this writing, it has 21,500 stars and more than 500 open issues. Yarn is compatible with the npm registry and has the same set of features as npm, but it operates faster and in a more reliable way. Let's take a look.

Installation

You can install yarn with npm but this is not recommended:

1npm install -g yarn
bash

Instead, if you're on Windows, download the installer (you need to have Node.js installed).

If you're using iOS or a Unix environment, the easiest way is via a shell script:

1curl -o- -L https://yarnpkg.com/install.sh | bash
bash

In this page you can find more information and other installation methods.

How it works

As we know, Node.js dependencies are placed in the node_modules directory of your project.

These dependencies are installed in a non-deterministically way, which means that the directory structure and dependency tree depend on the order dependencies are installed. Such a dependency is problematic because it can differ from machine to machine.

To solve this issue, Yarn uses a lockfile (yarn.lock) to ensure that everyone has the same version of every single file, resulting in the exact same file structure of the node_modules directory across all machines.

When dependencies are resolved, Yarn first looks in a local (global) cache to see if the package has been downloaded already. This, with Yarn's ability to efficiently queue up and parallelize requests, makes Yarn faster than npm.

So let's test it.

NPM vs Yarn

The comparisons outlined here were made using npm 4.05 and Yarn 0.18.1.

To initialize a project with npm we use npm init:

npm init

Yarn has the same init command, but with a slightly different set of questions and answers:

yarn init

To install a dependency and save it to package.json, for example express (which has more than twenty dependencies), in npm we execute:

1npm install --save express
bash

Using Yarn:

1yarn add express
bash

One little advantage of using Yarn is that you have to type less. Well, as long as you're not using aliases like npm i -S express.

But let's time the execution of these commands.

Npm:

time npm install

Yarn:

time yarn add

As you can see from the images, npm took 3.120 seconds, while Yarn took 2.588 seconds.

You can also see that the output of Yarn is more descriptive and prettier. The emojis are only available on Mac though.

Now, let's delete the node_modules directory and repeat the test to see Yarn's caching in action:

yarn cache usage

This time, Yarn took 1.455 seconds, an improvement of 44% over the previous execution. And 53% over npm.

Of course, you may get different results, but Yarn will beat npm every time.

We can also see that a yarn.lock file is generated (some parts are omitted for shortness):

1# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2# yarn lockfile v1
3
4
5accepts@~1.3.3:
6  version "1.3.3"
7  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
8  dependencies:
9    mime-types "~2.1.11"
10    negotiator "0.6.1"
11
12array-flatten@1.1.1:
13  version "1.1.1"
14  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
15
16content-disposition@0.5.1:
17  version "0.5.1"
18  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b"
19
20content-type@~1.0.2:
21  version "1.0.2"
22  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
23
24cookie-signature@1.0.6:
25  version "1.0.6"
26  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
27
28cookie@0.3.1:
29  version "0.3.1"
30  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
31
32debug@~2.2.0:
33  version "2.2.0"
34  resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
35  dependencies:
36    ms "0.7.1"
37
38depd@~1.1.0:
39  version "1.1.0"
40  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
41
42destroy@~1.0.4:
43  version "1.0.4"
44  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
45
46ee-first@1.1.1:
47  version "1.1.1"
48  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
49
50encodeurl@~1.0.1:
51  version "1.0.1"
52  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
53
54escape-html@~1.0.3:
55  version "1.0.3"
56  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
57
58etag@~1.7.0:
59  version "1.7.0"
60  resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8"
61
62express@^4.14.0:
63  version "4.14.0"
64  resolved "https://registry.yarnpkg.com/express/-/express-4.14.0.tgz#c1ee3f42cdc891fb3dc650a8922d51ec847d0d66"
65  dependencies:
66    accepts "~1.3.3"
67    array-flatten "1.1.1"
68    content-disposition "0.5.1"
69    content-type "~1.0.2"
70    cookie "0.3.1"
71    cookie-signature "1.0.6"
72    debug "~2.2.0"
73    depd "~1.1.0"
74    encodeurl "~1.0.1"
75    escape-html "~1.0.3"
76    etag "~1.7.0"
77    finalhandler "0.5.0"
78    fresh "0.3.0"
79    merge-descriptors "1.0.1"
80    methods "~1.1.2"
81    on-finished "~2.3.0"
82    parseurl "~1.3.1"
83    path-to-regexp "0.1.7"
84    proxy-addr "~1.1.2"
85    qs "6.2.0"
86    range-parser "~1.2.0"
87    send "0.14.1"
88    serve-static "~1.11.1"
89    type-is "~1.6.13"
90    utils-merge "1.0.0"
91    vary "~1.1.0"
92
93...
94
95vary@~1.1.0:
96  version "1.1.0"
97  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140"
js

Notice that Yarn uses https://registry.yarnpkg.com instead of https://registry.npmjs.org.

In a podcast episode of NodeUp (around the 40:30 mark) James Kyle (one of the main developers of Yarn) said that, for now, registry.yarnpkg.com is just a CNAME record that goes to the Cloudflare network and redirects to the NPM registry for experimental purposes, and to add some performance features.

Now compare it to the file npm-shrinkwrap.json (which serves a similar purpose than yarn.lock), generated when npm shrinkwrap is executed (some parts are omitted for shortness):

1{
2  "name": "npm",
3  "version": "1.0.0",
4  "dependencies": {
5    "accepts": {
6      "version": "1.3.3",
7      "from": "accepts@>=1.3.3 <1.4.0",
8      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz"
9    },
10    "array-flatten": {
11      "version": "1.1.1",
12      "from": "[email protected]",
13      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
14    },
15    "content-disposition": {
16      "version": "0.5.1",
17      "from": "[email protected]",
18      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.1.tgz"
19    },
20    "content-type": {
21      "version": "1.0.2",
22      "from": "content-type@>=1.0.2 <1.1.0",
23      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz"
24    },
25    "cookie": {
26      "version": "0.3.1",
27      "from": "[email protected]",
28      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz"
29    },
30    "cookie-signature": {
31      "version": "1.0.6",
32      "from": "[email protected]",
33      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
34    },
35    "debug": {
36      "version": "2.2.0",
37      "from": "debug@>=2.2.0 <2.3.0",
38      "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
39    },
40    "depd": {
41      "version": "1.1.0",
42      "from": "depd@>=1.1.0 <1.2.0",
43      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
44    },
45    "destroy": {
46      "version": "1.0.4",
47      "from": "destroy@>=1.0.4 <1.1.0",
48      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz"
49    },
50    "ee-first": {
51      "version": "1.1.1",
52      "from": "[email protected]",
53      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
54    },
55    "encodeurl": {
56      "version": "1.0.1",
57      "from": "encodeurl@>=1.0.1 <1.1.0",
58      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz"
59    },
60    "escape-html": {
61      "version": "1.0.3",
62      "from": "escape-html@>=1.0.3 <1.1.0",
63      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
64    },
65    "etag": {
66      "version": "1.7.0",
67      "from": "etag@>=1.7.0 <1.8.0",
68      "resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
69    },
70    "express": {
71      "version": "4.14.0",
72      "from": "express@>=4.14.0 <5.0.0",
73      "resolved": "https://registry.npmjs.org/express/-/express-4.14.0.tgz"
74    },
75
76	...
77
78    "vary": {
79      "version": "1.1.0",
80      "from": "vary@>=1.1.0 <1.2.0",
81      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.0.tgz"
82    }
83  }
84}
json

The advantage of using Yarn is that yarn.lock is generated automatically; with npm you have to execute npm shrinkwrap manually.

One important thing, yarn.lock should always be added to source control. Remember that this file ensures the deterministic installation of the dependencies.

Now, if you specify a package version, you'll notice an important difference in the generated package.json file.

For example, if you execute:

1npm install --save [email protected]
bash

It will generate:

1{
2  ...
3  "dependencies": {
4    "express": "^4.13.4"
5  }
6}
json

Executing:

1yarn add [email protected]
bash

Will generate:

1{
2  ...
3  "dependencies": {
4    "express": "4.13.4"
5  }
6}
json

Did you notice the missing caret(^) in Yarn's package.json file?

Many people recommended not using carets in our package.json files.

The caret will update the dependency to the most recent minor version when it's installed. For example, if there's a 2.4.1 version available, this will be installed even if ^2.3.1 is the one specified. This can cause trouble in some cases because sometimes breaking changes are introduced even in minor releases.

If you want to specify an exact version rather than using npm's semantic version range operator (the caret), you have to use the --save-exact (or its alias -E), as in:

1npm install --save-exact [email protected]
bash

However, that's something you have to know and keep in the back of your head. On the other hand, Yarn does this "caret-handling" for you. If this is something you want, it depends of your preferences and the libraries you're using.

Moving forward, if you already have a package.json file, you can install the dependencies with:

1yarn install
bash

Or just:

1yarn

If you want to remove a dependency and update the package.json file to reflect that, with npm you need to execute:

1npm uninstall --save express
bash

With Yarn, you need to execute:

1yarn remove express
bash

That will update your package.json and yarn.lock files by default.

The command yarn upgrade [package] will upgrade all the packages (or a single named package) to their latest version (some rules apply), and using yarn upgrade package@version will upgrade (or downgrade) an installed package to the specified version. In all cases, the yarn.lock file will be recreated as well.

Next I'll cover a key difference between npm and Yarn.

npm dedupe

One feature missing on Yarn is an equivalent of npm dedupe, a command to reduce duplicated dependencies. Since a project can have many versions of the same dependency, this dedupe command can come in handy.

For background, npm dedupe searches the dependency tree and attempts to simplify the overall directory structure by moving dependencies further up the tree (even if duplicates are not found), where they can be more effectively shared by multiple dependent packages. If a suitable version already exists at the target location, it will be left untouched, but the other duplicates will be deleted. This will result in both a flat and deduplicated tree.

On the other hand, the Yarn install command has a --flat flag:

1yarn install --flat
bash

On the first run, it will prompt you to choose a single version for each package that has more than one version on the dependency tree. These will be added to your package.json under a resolutions field. But this is not the same as npm dedupe, which removes duplicates by deletion.

This difference means that the node_modules directory generated by Yarn could take more space on disk than the one generated by npm.

However, this tradeoff might not impact you, your workflow, or your business significantly.

Other helpful commands in Yarn

This section covers other Yarn commands that you should know. You can see the complete list of Yarn commands and their options here.

For one, Yarn comes with a license checker. It will give you the license (and URL to the source code) associated with each package:

1yarn licenses ls
bash

yarn licenses

In addition to ls, yarn licenses generate-disclaimer will output the content of all licenses from all the packages you have installed.

To fetch information about a package, use yarn info:

1yarn info cookie
bash

yarn info

You can also ask for the version information, such as the currently installed version, the desired version based on semver, and the latest available version, of one or more packages of your packages.json file:

1yarn outdated
bash

yarn outdated

Finally, Yarn can show information about why a package, package folder, or file within a package folder is installed:

1yarn why xxx
bash

yarn why

Conclusion

Yarn is already used in production at Facebook. Furthermore, as demonstrated in this guide, it's a great high-performance alternative to npm.

For simple personal projects, perhaps the one you use doesn't make much of a difference. However, for bigger projects (and depending on your needs), Yarn might provide a clear advantage over the alternatives.