Yarn Plug'n'Play: Trading node_modules for Speed Without Breaking Your Build
Yarn Plug'n'Play eliminates node_modules, but the speed gain comes with compatibility costs. Here is how to evaluate it safely.
07 Aug 2025, 16:29 UTC

The node_modules tax
Every JavaScript project pays a tax in disk space and time. Run a standard install, and you get a node_modules directory that can hold hundreds of thousands of files. On continuous integration, that directory is created, used, and discarded on every run. Locally, it consumes space that many laptops cannot spare. Yarn's Plug'n'Play (PnP) mode attacks this problem directly by removing node_modules entirely. The question is whether your project can survive without it.
What PnP actually does
In standard Node.js resolution, when your code calls require('lodash') or import _ from 'lodash', Node walks up the directory tree looking for a matching folder inside node_modules. Yarn PnP replaces this file-system search with a generated resolution map.
When you run yarn install in a PnP-enabled project, Yarn creates a .pnp.cjs file at the project root. This file acts as an interceptor. Instead of letting Node traverse directories, it reads the map inside .pnp.cjs and points the request directly to a zip archive stored under .yarn/cache. The package contents remain compressed until they are needed.
The operational result is a smaller working directory and a faster install step. There is no massive tree of individual files to write to disk. For teams running frequent clean checkouts in CI, this can reduce both build minutes and disk churn.
The compatibility cliff
The speed gain comes from a strict assumption: every consumer of your dependencies understands PnP. Many tools do not. The Node ecosystem spent years hardcoding paths like ./node_modules/.bin/ or reading package.json files from physical directories. Under strict PnP, those paths do not exist.
Common casualties include native add-ons that expect to be extracted before execution, build scripts that shell out to binaries inside node_modules, and linters or test runners that resolve configuration files by walking the file tree. Yarn provides two escape hatches. The first is packageExtensions, which lets you patch dependency metadata inside the resolution map. The second is unplugging, which extracts a specific package into a real directory.
It is also worth noting the version boundary. PnP is the default install mode in Yarn 2 and later. Yarn 1 does not use it. If your project still runs Yarn 1, adopting PnP means a major-version migration, not a configuration flag.
Worked example: unplugging a stubborn dependency
Suppose your production build fails after switching to PnP. The error suggests a tool cannot find its own assets because it expects a physical node_modules layout. You have confirmed the package is PnP-incompatible for now. The targeted fix is to unplug it.
Run the following command from your project root. You need write access to the .yarn directory:
yarn unplug some-build-tool
Replace some-build-tool with the actual package name causing the failure. After the command completes, verify the result:
- Check that a directory now exists under
.yarn/unplugged/containing the extracted package. - Re-run the failing build step and confirm the resolution error is gone.
The risk is that you lose some PnP benefits for that specific package. It now occupies real disk space and participates in normal file-system resolution. Keep the unplug list as short as possible, and revisit it when the upstream package adds PnP support.
If you need to revert the change, remove the extracted directory under .yarn/unplugged/ and run yarn install again to regenerate the resolution map without that override.
Zero-Installs: shifting cost into Git
PnP makes an aggressive workflow possible: Zero-Installs. Because dependencies live in .yarn/cache as zip archives and the resolution logic lives in .pnp.cjs, you can commit both to version control. A fresh clone then has everything it needs without running an install step first.
The trade-off is repository size. Committing the full cache moves storage cost from local disk into Git history. Before adopting Zero-Installs, measure the impact:
- Note your repository size before committing the cache.
- Commit
.pnp.cjsand.yarn/cache. - Compare the repository size again, and check clone times.
If the cache is large and your team clones frequently, the convenience may not be worth the history bloat. Some teams commit only .pnp.cjs and restore the cache from a remote artifact store instead.
How to evaluate PnP safely
Do not flip the switch on your main branch and hope for the best. Create a branch and run a deliberate verification pass:
| Check | What to look for |
|---|---|
| Clean install | Run yarn install in a fresh clone. Confirm that no node_modules directory is created and that .pnp.cjs is generated. |
| Full test suite | Execute every test target. Module resolution errors often appear in edge cases, not just the main entry point. |
| Production build | Run the exact build command your deployment uses. Build scripts are the most common PnP breakage point. |
| Repository size | If considering Zero-Installs, compare repository size before and after committing .yarn/cache. |
If any check fails, unplug the specific package and re-test. If failures are widespread, PnP may not fit your current toolchain yet. The mode is a genuine engineering trade-off, not a universal upgrade.
Bottom line
Yarn PnP replaces the node_modules directory with a generated resolution map and a zipped package cache. It can make installs faster and working directories smaller, but it demands that your tools either understand PnP or be explicitly unplugged. Treat it as a compatibility project: test your full pipeline, keep unplugged packages to a minimum, and measure the Git cost before committing to Zero-Installs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.