Why pnpm Exists: Rethinking node_modules
If you work with JavaScript or TypeScript, npm install was probably one of the first commands you learned. It is simple, familiar, and, most importantly, it works.
So why would anyone create another package manager?
Let’s find out.
What npm actually does
NPM, short for Node Package Manager, is the default package manager for Node.js, which solves many of the problems that developers faced a decade ago. It provides lockfiles, workspaces, security auditing, and multiple installation strategies.
At a high level, npm takes the dependency information from your package.json and lockfile and uses it to build the node_modules directory on your disk:
- npm determines which packages are required;
- which versions of those packages should be installed;
- where each package should live inside node_modules.
The tricky part is that dependencies can overlap. If two packages require the same version of a dependency, npm can usually install a single copy and let both packages share it. But if they require different versions, npm may need to install multiple copies of that dependency.
To minimize this duplication:
- npm uses hoisting: when a dependency can be safely shared, npm moves it higher in the node_modules tree. This allows multiple packages to resolve the dependency from the same location.
- npm dedupe command: it can reorganize the installed dependency tree. It allows packages to share dependencies whenever possible.
In other words, npm is not simply copying every dependency into node_modules. It is solving a dependency-resolution problem: it must find a directory structure that satisfies the entire dependency graph while sharing as many packages as possible and avoiding unnecessary duplication.
And that’s where the real complexity begins.
The hidden cost of node_modules
The traditional node_modules layout is convenient, but it has two important drawbacks.
1. Dependencies can be duplicated
Real dependency graphs often contain different versions of the same package. With hundreds of dependencies, this can produce many files and filesystem operations, especially when installing in CI, switching branches, or working across multiple applications.
2. Hoisting can hide missing dependencies
Suppose your application declares package-a, and package-a depends on lodash. Your application does not declare lodash itself. Because of hoisting, the root node_modules might still contain it, allowing the application to import it.
This is a phantom dependency: the application uses a package that it never declared.
The problem appears when package-a changes and no longer depends on lodash. The application can suddenly fail with Cannot find module ‘lodash’. Nothing in the application changed; it was relying on another package’s internal dependency.
Thus, while node_modules is convenient, hoisting can make dependency boundaries less explicit.
So node_modules is convenient, but that convenience comes with a cost: duplicated files, filesystem work, and dependencies that can appear to exist when they don’t actually belong to your project.
So what exactly is pnpm?
pnpm, short for Performant npm, is another package manager for the Node.js ecosystem. Its key idea is a content-addressable store.
Imagine three projects all using the same version of React. Instead of maintaining three independent physical copies, pnpm stores the package contents in a shared store and reuses them across projects.
When a project needs those files, pnpm can create hard links to the store instead of copying the data. A hard link lets multiple paths refer to the same underlying data, so many projects can reuse the same package files.
This saves disk space.
Hard links and symlinks
Hard links solve the storage problem, but pnpm also needs Node.js to resolve dependencies correctly. This is where symbolic links, or symlinks, are used.
The core idea is:
Hard links → reuse package data;
symlinks → construct the dependency graph.
Node.js does not require dependencies to physically exist in a traditional flat node_modules directory. pnpm uses Node’s module-resolution behavior to create a different layout while remaining compatible.
Get Margarita Zakharova’s stories in your inbox
Join Medium for free to get updates from this writer.
This also helps solve phantom dependencies:
- a package can access the dependencies it declares;
- the application does not automatically gain access to every transitive dependency;
- package.json becomes a more accurate description of what the application actually needs.
Installation and reuse
Conceptually, pnpm installation has three stages:
- Resolve dependencies.
- Calculate the required directory structure.
- Link dependencies from the store into node_modules.
If you delete node_modules and run pnpm install again, packages already present in the store can be reused rather than downloaded and copied from scratch.
Monorepos and shared projects
If several applications share packages such as React, TypeScript, ESLint, or Vite, a traditional setup can store the same package files repeatedly. With pnpm, each project still has its own dependency structure, while the underlying files can be shared through the common store.
This is especially useful with monorepos: pnpm workspaces allow applications and shared packages to coexist in one dependency graph and link local packages directly.
Reproducibility
Another important benefit is predictable dependency resolution.
Suppose package.json contains: “some-library”: “³.0.0”
The ^ range allows multiple compatible releases. Without a lockfile, the resolved dependency tree can change as new versions are published.
Lockfiles solve much of this problem by recording the actual resolution. npm uses package-lock.json; pnpm uses pnpm-lock.yaml. The lockfile records the resolved dependency graph, helping development and CI environments reproduce the same installation.
Is pnpm always faster?
Not necessarily. Installation performance depends on cache state, network speed, dependency count, disk performance, CI environment, filesystem, and registry performance.
pnpm can have a significant advantage when packages are already available in its store because linking existing files can be cheaper than repeatedly copying them. But a completely new dependency graph still has to be downloaded. There is no magic performance boost.
What are the downsides?
pnpm has trade-offs.
First, some tooling expects flat node_modules. Older tools may assume dependencies are hoisted into the root directory. pnpm’s isolated layout can expose these assumptions, although configuration options can help with legacy tooling.
Second, migration can reveal broken dependency declarations. An old project may work with npm because it accidentally relies on a transitive dependency. Switching to pnpm can expose this missing declaration. That can be annoying, but it also fixes a real dependency-management problem.
Third, filesystem behavior matters. pnpm relies on hard links and symlinks, so unusual filesystems, containers, or deployment platforms may impose restrictions. Production and CI environments should be tested before adopting pnpm everywhere.
What about security?
pnpm does not magically make JavaScript dependencies secure. A different filesystem layout cannot make a malicious package trustworthy.
Dependency isolation can improve dependency hygiene by making the graph more explicit, and pnpm supports auditing and overrides for vulnerable transitive dependencies. Still, dependency isolation ≠ dependency security. Production systems should continue to use lockfiles, vulnerability scanning, CI checks, dependency review, and careful monitoring.
Should everyone use pnpm?
The short answer is “No”.
For a small Node.js project with only a few dependencies, npm is perfectly reasonable. There is little value in adding complexity simply because another tool is fashionable.
pnpm becomes more attractive when you have many projects on one machine, large dependency trees, large monorepos, multiple applications sharing dependencies, significant CI installation costs, concerns about undeclared dependencies, or substantial node_modules disk usage.
For a large TypeScript monorepo, pnpm can be a compelling choice. For a tiny script, npm may be all you need.
The bigger idea behind pnpm
The interesting part of pnpm is not the command pnpm install. It is the question behind it:
Why should every project own another physical copy of the same dependency?
The traditional node_modules model prioritized simplicity and compatibility, but its costs become more visible as applications grow. pnpm stores package contents centrally, reuses them through hard links, and connects them into each project’s dependency graph with symlinks. Projects still behave as if they have their own dependencies, while the underlying data is shared and dependency boundaries remain explicit.
Final thoughts
npm is not bad. Modern npm has evolved considerably and provides many of the tools developers need. But its traditional hoisted node_modules model can lead to duplicated files, filesystem overhead, and dependencies that appear available even when they are not declared.
pnpm takes a different approach: it stores package contents centrally, reuses them through hard links, constructs dependency relationships with symlinks, and keeps dependency boundaries more explicit.
The result is not simply “npm, but faster.” It is a different model for installing and managing dependencies: sometimes the best optimization is to stop doing the expensive thing in the first place.










