The reason for switching from npm to Yarn Berry:
Index
Recently, our team adopted Yarn Berry (v2) for dependency management. I’d like to share the background behind introducing this new package manager and some of the benefits we’ve experienced while using it.
Yarn Berry?
Yarn Berry is a new package management system for Node.js, created by Maël Nison, one of the main developers of Yarn v1. Since its official release (v2) on January 25, 2020, it has been adopted by major open-source repositories like Babel. Its source code is maintained in the yarnpkg/berry repository on GitHub.
Yarn Berry introduces a groundbreaking improvement over the traditional, often “broken,” npm package management system.
Problems with npm
Although npm comes bundled with Node.js and is widely used, it has many inefficient and even broken aspects.

Inefficient dependency resolution
npm manages dependencies through the file system, most notably using the familiar node_modules folder. However, this approach makes dependency resolution inefficient.
For example, let’s say you’re importing a package with require() from a certain folder.
To see the list of directories Node.js traverses while searching for that library, you can use the built-in function require.resolve.paths(). This function returns the list of directories that npm checks during its lookup process.
➜ server git:(main) node
Welcome to Node.js v22.19.0.
Type ".help" for more information.
> require.resolve.paths('nodejs')
[
'/Users/lim/Documents/dev/inflearn-food-map-app/server/repl/node_modules',
'/Users/lim/Documents/dev/inflearn-food-map-app/server/node_modules',
'/Users/lim/Documents/dev/inflearn-food-map-app/node_modules',
'/Users/lim/Documents/dev/node_modules',
'/Users/lim/Documents/node_modules',
'/Users/lim/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/lim/.node_modules',
'/Users/lim/.node_libraries',
'/Users/lim/.nvm/versions/node/v22.19.0/lib/node',
'/Users/lim/.node_modules',
'/Users/lim/.node_libraries',
'/Users/lim/.nvm/versions/node/v22.19.0/lib/node'
]As you can see from the list, npm searches upward through each parent directory’s node_modules folder to find a package.
The longer it takes to locate the package, the more it repeats slow I/O operations such as readdir and stat. In some cases, these I/O calls can even fail partway through.
Behavior that varies by environment
npm keeps searching parent directories’ node_modules folders when it can’t find a package. Because of this behavior, which dependencies are actually resolved depends on the node_modules structure in the parent directories.
For example, whether or not a dependency can be imported may vary depending on which node_modules exist in higher-level folders. There’s even a risk of pulling in the wrong version of a dependency.
This kind of environment-dependent behavior is a bad sign—it makes issues harder to reproduce consistently.
Inefficient installation
In npm, the node_modules directory structure takes up a huge amount of space. Even a simple CLI project often requires hundreds of megabytes in its node_modules folder. Beyond just disk usage, creating such a massive structure also involves heavy I/O operations.
Because the node_modules tree is so complex, it’s difficult to verify whether an installation is truly valid. For instance, with hundreds of interdependent packages, the directory structure quickly becomes deeply nested.
Checking whether all dependencies in such a deep tree are installed correctly would require a large number of I/O calls. Since disk I/O is much slower than working with in-memory data structures, both Yarn v1 and npm typically stop at validating the dependency tree itself. They don’t go as far as verifying whether each package’s contents are actually correct.
Phantom Dependency
In npm and Yarn v1, a technique called hoisting is used to reduce duplicate node_modules installations.
For example, let’s say the dependency tree looks like the one on the left.
In that tree, the packages [A (1.0)] and [B (1.0)] are installed twice, wasting disk space. To save space, npm and Yarn v1 transform the original tree into the one on the right.
After this transformation, package-1 can now require() [B (1.0)], even though it wasn’t supposed to be able to in the original tree.
This phenomenon—where a package can import a library it doesn’t explicitly depend on—is called a phantom dependency.
When phantom dependencies occur, you can end up quietly using libraries that aren’t listed in your package.json. And if you later remove another dependency from package.json, those phantom dependencies may silently disappear as well. This behavior makes dependency management confusing and unreliable.
Plug'n'Play (PnP)
Yarn Berry addresses the issues mentioned above with a new strategy called Plug’n’Play (PnP).
Background
Yarn v1 builds the dependency tree from the package.json file and then creates a node_modules directory structure on disk. In other words, the dependency graph is already fully known.
Managing dependencies through the node_modules file system is fragile. what if package managers could handle dependencies in a safer, more fundamental way?
That line of thinking led to the creation of Plug’n’Play (PnP).
How Plug’n’Play works
When you run yarn install in Plug’n’Play mode, you’ll notice a very different setup compared to the traditional approach.

Yarn Berry does not create a node_modules folder.
Instead:
- Dependency data is stored in the
.yarn/cachefolder. - Lookup information is recorded in the
.pnp.cjsfile.
With .pnp.cjs, you can instantly determine which package depends on which library and where each library is located—without relying on disk I/O.
Yarn overrides the way Node.js normally handles the require() statement, allowing it to resolve packages more efficiently. Because of this, when managing dependencies through the PnP API, you should use the yarn node command instead of the standard node command.
$ yarn nodeZipFS (Zip Filesystem)
In the Yarn PnP system, each dependency is stored as a ZIP archive. For example, Zustand version 0.1.2 is managed as a compressed file like: zustand-npm-0.1.2-9a0edbd2b9-c69105dd7d.zip. After that, the contents of the ZIP archives are dynamically referenced according to the instructions in the .pnp.cjs file.
Managing dependencies as ZIP archives brings several advantages:
- Since there’s no need to generate a
node_modulesdirectory structure, installation completes much faster. - Each package version is stored as a single ZIP archive, eliminating duplicate installations. Because the archives are compressed, storage usage is greatly reduced.
- In fact, our team was able to significantly cut down dependency size.
- Because the number of files that make up the dependencies is relatively small, detecting changes or removing all dependencies can be done much more quickly.
- When the contents of a ZIP file change, the modification can be easily detected by comparing it against its checksum.
- It’s easy to identify missing dependencies or those that are no longer needed.
Results of adopting Plug’n’Play
As a result of adopting Plug’n’Play, our team was able to experience a variety of benefits.
When resolving dependencies
When resolving dependencies, there’s no longer any need to traverse the node_modules folder. Instead, the .pnp.cjs file provides a data structure that allows the exact location of a dependency to be found immediately. As a result, the time required for require() calls has been significantly reduced.
Maintaining a consistent environment
Since all package dependencies are managed through the .pnp.cjs file, they are no longer affected by external environments. This ensures that the behavior of require() or import statements remains consistent across different machines and CI environments.
Strict dependency management
Yarn PnP does not hoist dependencies the way node_modules does. This means each package can only access the dependencies explicitly listed in its own package.json. Code that might have “worked by accident” under different environments is now strictly controlled. As a result, the phantom dependency issue—previously a common source of unexpected bugs—has been fundamentally eliminated.
Saving CI dependency installation time with Zero-Install
Normally, when no cache is available, installing dependencies takes around 60–90 seconds. With Zero-Install, however, dependencies are immediately available right after cloning the repository with git clone, eliminating the need for a separate installation step. This led to a significant reduction in CI time.
Conclusion
By adopting Yarn Berry, our team was able to manage JavaScript dependencies more efficiently and securely. They also reduced CI times by more than 60 seconds.