March 2023
4 minute read time
Getting started with pnpm to reduce your package installation time 10x
Martin Torp
By Martin Torp
Cofounder of Coana
PhD in Computer Science

<m>At Coana we recently migrated from npm to pnpm.
The migration was triggered by a desire to merge multiple separate repositories into a single repository with multiple subprojects (a mono repository), which we would manage using [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces).
Having some experience with workspaces from another project, we were confident that they would also suit our needs in this case.
However, we quickly started to experience a typical issue with npm's module layout algorithm.</m>

<m>In a workspace project, you have one `package.json` in the root, and one in each subproject, which are typically placed in a folder called `packages`, for example, `./packages/backend`, `./packages/frontend`, and so on.
There is a `node_modules` folder in the root and one in each of the subprojects.
To save space on the drive, npm will move packages used by several of the subprojects to the root `node_modules`, a process called hoisting.
If we take `./packages/frontend` as an example, some of its dependencies will be installed in `./node_modules` and some in `./packages/frontend/node_modules`.
Hoisting generally does not affect application behavior, but applications that use phantom dependencies (a topic, which we covered [here](https://www.coana.tech/post/a-quick-introduction-to-phantom-dependencies)) may break.
In our case, we faced several problems caused by phantom dependencies, and since npm, to the best of our knowledge, does not yet have a mechanism to resolve the issues, we decided to give pnpm a go.</m>

<m>Having heard lots of praise from other developers about pnpm (especially its performance), we were excited to try the package management tool.
pnpm installs all packages in the folder `./node_modules/.pnpm` and then adds symlinks to these folders from the various `node_modules` folders.
For example, if both `./packages/backend` and `./packages/frontend` depend on chalk v5.2.0, the following symlinks are created:</m>

<m>```
./packages/backend/node_modules/chalk -> ./node_modules/.pnpm/chalk@5.2.0
./packages/frontend/node_modules/chalk -> ./node_modules/.pnpm/chalk@5.2.0
```</m>

<m>The advantages of this approach are twofold.
First, it preserves the original and more natural hierarchical `node_modules` structure (as opposed to the flat structure used by npm by default).
Second, it ensures that packages are deduplicated (deduped), such that the same version of the same package does not appear multiple times in the same application.</m>

<m>## What we did</m>

<m>The switch to pnpm was extremely easy and fast.
We [installed pnpm](https://pnpm.io/installation) and ran `pnpm install` to install our dependencies and generate the `pnpm-lock.yaml` pnpm lock file.
Since we use workspaces to manage a mono repository, we had to create the configuration file `pnpm-workspace.yaml`, which contains a single key `packages` that holds an array of string paths of the workspaces packages. For example:</m>

<m>```
packages:
 - 'packages/backend'
 - 'packages/frontend'
```</m>

<m>Each of the workspace projects has its own `package.json`, but there is only a single lock file (`pnpm-lock.yaml`), which is stored in the root of the project.
Remember to commit this lock file such that installations are reproducible on other machines (You may also want to read our guide on working with lock files [here](https://www.coana.tech/post/navigating-lock-files-best-practices-and-tips)).</m>

<m>The last thing we had to do to complete the transition to pnpm was to configure pnpm to allow some phantom dependencies (see our intro to phantom dependencies [here](https://www.coana.tech/post/a-quick-introduction-to-phantom-dependencies)).
By default, pnpm does not allow any phantom dependencies and will throw a module not found error if you try to use one.
Since we use [firebase](https://www.npmjs.com/package/firebase), where [@firebase/app](https://www.npmjs.com/package/@firebase/app) is intended to be used as a phantom dependency, we had to configure pnpm to install @firebase/app in a way where it can be used as a phantom dependency.
We did that by adding the following line to `.npmrc`:</m>

<m>```
public-hoist-pattern[]=*firebase*
```</m>

<m>This line tells pnpm to install all dependencies containing firebase in their name in the root `node_modules`, such that these packages can be loaded from all other packages.</m>

<m>The switch to pnpm has resulted in a tremendous performance boost.
The `pnpm install` command runs in less than a second when packages were previously installed - this took more than 10 seconds with `npm install`.
In our GitHub action build, we have configured pnpm to use a cache using [this approach](https://github.com/pnpm/action-setup#use-cache-to-reduce-installation-time).
Package installation now only takes 15 seconds (including the time it takes to fetch the cache) when running in a GitHub action.
We encourage you to give pnpm a go and see if it performs as well for you.</m>

Questions or opinions?

Feel free to reach out to us by email or through our Slack Community anytime. We'd love to hear from you.

Join waitlist

Sign up to get notified about future Coana future launches.