JavaScript applications are known for having a lot of dependencies. And those dependencies are usually managed through a package manager. By default node uses NPM. However, NPM lacks some advanced features that are great for more advanced applications or is slow in installing packages or solving packages dependencies.
To solve the above, the community made new package managers, such as Yarn and pnpm. Yarn is probably the most used alternative, but lately is becoming slower. The newest kid in town, pnpm, was a new way to manage package cache that makes it faster on installing/upgrading packages.
In this article, we intend to analyze the performance of those three packages management options and define a new standard to be used by Pixelmatters Engineering Team.
Why not keep Yarn?
Some years ago, we decided to use Yarn as our default package manager. However, after NPM released its version 5, it became more similar to Yarn, and the need for it disappeared in most of the projects.
At the end of the day, Yarn is just a small improvement over NPM, but most things work exactly the same. They don't panne to move away from a flattened dependency tree, which allows modules to access packages they don't depend on or solve the space usage issue.
Having multiple node projects on the machine means losing valuable Gigabytes for numerous copies of the same package on those different projects. For example, if we have 10 projects depending on the same Ramdajs version, there are 10 copies of the dependency for each module.
Why is pnpm an option?
pnpm has three big goals — saving disk space, boosting installation speed, and creating a non-flat node_modules directory.
These proposals stand out in relation to the other package managers. People who have many JavaScript projects on their machine will notice that disk space is a concern; since NPM and Yarn install the same dependencies for all the projects, there's no central storage here application can reuse the code from.
In my opinion, the other main advantage is installation time, which is related to the non-flat node_module directory and having central storage that contains the already downloaded dependencies. The algorithms for creating a flat tree are much simpler than the ones used by NPM and Yarn, and even the way the lock files are generated makes it faster to process.
The other reason is that having a central store with all downloaded packages and dependencies already processed makes it faster to assign them to the project where pnpm is installing dependencies.
Performance comparison
We ran the tests with a package.json with more than 100 dependencies to cover times for the different scenarios:
- clean install: installed for the first time with no cache on the system;
- re-install: install after the first install;
- with cache and lockfile: remove node_modules and run install;
- with cache: remove node_modules, lock file, and run install;
- with lockfile: remove node_modules and cache, and run install;
- with cache and node_modules: only remove lockfile and install gain;
- with node_modules and lockfile: remove cache and run install again;
- with node_modules: remove cache and lockfile, and install again.
The testing environment was the following:
- MacBook Pro 15", 2018
- Processor Intel Core i7 2,2GHz 6 core
- Memory 16GB 2400 MHz DDR4
- macOS 11.5.2
- Node version: 14.15.3 (LTS)
Package managers version:
- Npm: 7.20.6
- Yarn: 1.22.10
- Pnpm: 6.14.7
How can it be this fast?
pnpm doesn't use a flatten dependency tree, so its algorithms can be a lot easier to implement, maintain, and requires less computation. NPM used this type of tree before version 3, but there were many issues on windows due to the nesting on the directories paths, and packages had to be copied several times for each package that depended on it.
pnpm solved these issues by using symbolic links that point to a central offline repository with all previously installed packages. This makes really fast installing dependencies on projects as well as better preserves disk space. The directories are not as deep as on the npm2 since pnpm keeps all dependencies flat but uses symlinks to group them together.
Possible limitations
Every choice comes with a consequence. Fortunately, pnpm has a few downsides, and none of them affect how we use a package manager. Let's do a quick enumeration of those downsides:
- Since pnpm uses a flat tree the lock files produced by NPM aren't supported. But there is a command that can convert the NPM/Yarn lock file into a pnpm one. See more here.
- pnpm can't publish packages with the bundledDependencies. Nevertheless, this isn't a recommended feature to use even on NPM. The best is to use a package bundler, like webpack, rollup, or ESBuild.
The other known limitations are even more niche compared with the above, so there is no need to talk about them. The most probable thing is never to run into an issue.
Conclusion
pnpm can be a good alternative to yarn and npm, especially when looking at its speed. It can quickly install dependencies locally, but we can also include it on the CI to speed up our pipeline. The Engineering Team is currently trying out pnpm in some new projects to understand if it can become one of our internal standards as a package manager.