JavaScript package managers have evolved far beyond installing dependencies. Today, tools like npm, Yarn, and pnpm also manage workspaces, generate lockfiles, audit vulnerabilities, and shape how teams secure their software supply chain.
That last point matters more than ever. The npm ecosystem has become a high-value target for supply chain attacks, from compromised maintainer accounts to malicious lifecycle scripts that run during installation. Recent incidents such as the Axios supply chain attack and the Mini Shai Hulud malware campaign show how quickly a trusted dependency can become an attack path. OWASP now lists software supply chain failures as A03 in its 2025 Top 10, reflecting how central dependency risk has become to application security.
npm still defines many of the conventions the JavaScript ecosystem uses every day, including package.json, lockfiles, lifecycle scripts, and registry publishing. But some of npm’s long-standing design choices are showing their age, especially around node_modules bloat, dependency hoisting, install-time trust, and default security posture.
pnpm takes a different approach. Instead of copying dependency files into every project, it stores packages once in a content-addressable global store and links them into each project. That architecture reduces disk usage, speeds up repeated installs, and avoids some of the accidental dependency access that npm’s flattened node_modules layout can allow. More recently, pnpm has also become more opinionated about security by delaying newly published versions, blocking unapproved build scripts, and rejecting certain risky dependency sources by default.
In this article, we’ll look at where pnpm improves on npm, how its node_modules structure works, what its newer security defaults protect against, and what to consider before migrating an existing project.
pnpm is not just “npm, but faster.” Its main advantage is that performance, disk efficiency, and dependency strictness come from the same architectural choice: packages are stored once and linked into projects instead of being duplicated or broadly hoisted.
The table below summarizes the key differences:
| Area | npm | pnpm |
|---|---|---|
| Dependency storage | Installs packages into each project’s node_modules |
Stores package files once in a global content-addressable store and links them into projects |
| Disk usage | Can duplicate package files across projects | Reuses the same package version across projects |
| Dependency access | Hoisted dependencies can be imported even when they are not declared directly | Strict layout makes undeclared dependency imports easier to catch |
| Install scripts | Lifecycle scripts run by default unless restricted | Dependency build scripts require explicit approval through allowBuilds |
| Newly published versions | Can be delayed with min-release-age, but this is opt-in |
Delays newly published versions by default in pnpm 11 |
| Workspaces | Supported through the workspaces field |
Supported through pnpm-workspace.yaml, with centralized security and version policy settings |
| SBOM support | Supports npm sbom |
Supports pnpm sbom |
The trade-off is compatibility. pnpm’s strictness can expose problems that npm’s hoisting hides. That is usually a good thing for long-term correctness, but it can make migration noisier for projects that rely on undeclared dependencies.
The package install step is one of the most trusted parts of a JavaScript workflow. It is also one of the riskiest.
When you run npm install, dependency lifecycle scripts such as preinstall, install, and postinstall can execute automatically. Legitimate packages use these scripts to download binaries, compile native modules, or perform setup work that the package needs in order to function.
The problem is that install scripts are still third-party code running on your machine or in CI. If an attacker compromises a package, maintainer account, or release process, the install step becomes a direct execution path.
The Mini Shai Hulud campaign exploited that kind of trust boundary: malicious packages used installation behavior to steal sensitive credentials such as GitHub tokens, API keys, and cloud secrets. Once those credentials were captured, attackers could publish additional poisoned packages and expand the compromise.
Lockfiles help reduce the risk of silently picking up a malicious patch release. For example, if a project depends on axios with a semver range such as ^1.13.0, installing without a lockfile can resolve to a newer patch or minor version that satisfies the range. If that newer version is compromised during the window when it is available, your project may install it automatically.
That is why package manager behavior matters. Security is not only about running npm audit after installation; it is also about deciding which code is allowed to run during installation and how quickly newly published versions can enter your dependency tree. Understanding why npm dependencies are a bigger security risk than your code can help frame just how significant these decisions are.
pnpm’s newer security defaults are designed around a simple idea: the package manager should not assume that every newly published package, transitive dependency, or install script is safe.
These defaults do not make a project immune to supply chain attacks. You still need lockfiles, code review, two-factor authentication for package publishing, dependency monitoring, and secret scanning. But pnpm shifts more protection into the default install path instead of requiring every team to opt into it manually.
minimumReleaseAgepnpm 11 sets minimumReleaseAge to 1440 minutes by default, which means a newly published package version must be at least 24 hours old before pnpm will resolve it.
That delay helps with a common supply chain pattern: malicious versions are often discovered and removed shortly after publication. Waiting a day gives maintainers, registries, and security tools time to react before the version reaches your project.
You can increase the delay in pnpm-workspace.yaml:
minimumReleaseAge: 10080 # Wait one week before installing newly published versions
You can also exempt trusted packages, such as internal packages your organization publishes:
minimumReleaseAge: 1440 minimumReleaseAgeExclude: - '@your-org/*'
npm has added a similar setting, min-release-age, but it is not enabled by default. That difference matters: with pnpm, the safer behavior is the starting point; with npm, teams must choose to configure it.
Install scripts are powerful because they run code during dependency installation. That power is also why they are dangerous.
pnpm’s allowBuilds setting lets teams approve which dependencies may run lifecycle build scripts. If a package tries to run a build script and is not allowlisted, pnpm reports it instead of silently trusting it.
A typical workflow looks like this:
pnpm install pnpm approve-builds
The resulting policy lives in pnpm-workspace.yaml:
allowBuilds: '@swc/core': true esbuild: true sharp: true
This is useful because many legitimate packages need build scripts. Native dependencies such as sharp, esbuild, and @swc/core may require postinstall behavior. The goal is not to ban lifecycle scripts entirely; it is to make script execution explicit and reviewable.
npm has also introduced an approval workflow through npm approve-scripts, but in the current npm release this policy is advisory: install scripts still run by default, and npm reports which packages have not been reviewed.
Another pnpm 11 default, blockExoticSubdeps, blocks transitive dependencies from resolving through sources such as Git repositories or tarball URLs. These sources can be legitimate, but they also bypass some of the normal expectations teams have around public registry packages, provenance, and review.
This does not mean you can never install from a Git URL. It means pnpm is stricter about allowing a package deep in your dependency tree to pull code from a less predictable source without you noticing.
pnpm 10.21 introduced trustPolicy, including a no-downgrade mode that can block a package version if its trust evidence is weaker than a version you previously installed.
For example, if a package previously shipped with provenance and a later version does not, pnpm can refuse the downgrade in trust. That kind of policy is especially relevant after incidents where compromised package versions lack the provenance signals present in earlier legitimate releases.
npm can display provenance information and supports signature verification through commands like npm audit signatures, but it does not currently enforce a comparable trust downgrade policy during normal installation.
The other major difference between npm and pnpm is how they store packages on disk.
A JavaScript dependency rarely arrives alone. Install express, and you also install the packages express depends on, the packages those packages depend on, and so on. This transitive tree can grow quickly.
npm v3 improved disk usage by flattening node_modules and hoisting dependencies to the project root where possible. That reduced duplication, but it also introduced a long-standing side effect: packages can sometimes be imported by your application even if you never declared them in package.json.
For example, suppose express depends on debug. With npm’s flattened layout, debug may appear at the top level of node_modules. Your application can then accidentally do this:
import debug from 'debug';
The import works even though debug is not your direct dependency. Later, if express removes debug or changes how it depends on it, your application breaks. This is the “phantom dependency” or “ghost dependency” problem. Tools like Knip can help you find unused and ghost dependencies before they cause issues in production.
pnpm avoids this by using a stricter node_modules layout. Package files are stored once in a global content-addressable store. Projects then reference those files through hard links and symlinks.
A simplified layout looks like this:
~/.pnpm-store/
└── v11/
├── files/
│ ├── 4a/1f9c8b... ← react content, hash-addressed
│ └── 7e/2d5a91... ← react-dom content, hash-addressed
└── index.db
project-a/
└── node_modules/
├── react -> .pnpm/[email protected]/node_modules/react
├── react-dom -> .pnpm/[email protected]/node_modules/react-dom
└── .pnpm/
├── [email protected]/
│ └── node_modules/
│ └── react/ ← hard linked from the store
└── [email protected]/
└── node_modules/
├── react-dom/ ← hard linked from the store
└── react -> ../../[email protected]/node_modules/react
A hard link points another file name at the same underlying data on disk, so the package contents are not duplicated. A symlink works more like a shortcut, pointing one path to another. pnpm combines both: hard links avoid repeated package contents, and symlinks preserve the dependency relationships Node needs for module resolution.
This is why pnpm can feel dramatically lighter on machines with many projects. If 20 projects all use the same version of React, pnpm stores that version once and links it into each project as needed.
One caveat: Finder, Windows Explorer, and some disk-usage tools can make pnpm projects look larger than they really are because they count each hard-linked file as if it were a separate copy. To measure actual disk usage, use tools that understand hard links or compare free disk space before and after installation.
node_modules bloatEarly in my career, I dealt with node_modules bloat before I understood what caused it. I was learning React on a 256GB laptop with limited internet access, so I copied entire node_modules folders between projects to avoid downloading the same packages again.
pnpm solves that problem at the package manager level. Whether you have one project or a hundred on the same machine, every package version is stored once and reused.
I saw this in a large pnpm monorepo with four apps. The root node_modules appeared to take about 1.87GB on disk. After deleting node_modules and reinstalling, the free storage on my machine barely changed because the package contents were already in pnpm’s store. The reinstall mostly recreated links.
When I converted the same project to an npm monorepo and installed dependencies, disk usage increased because npm wrote a separate copy of the dependencies into that project.
The important lesson is not just “pnpm is smaller.” It is that pnpm treats installed packages as reusable content, while npm generally treats each project install as its own dependency tree.
pnpm’s workspace configuration has become a central place for dependency policy.
In pnpm 11, most non-registry settings belong in pnpm-workspace.yaml rather than being scattered across .npmrc files or a pnpm field in package.json. That makes settings such as minimumReleaseAge, allowBuilds, blockExoticSubdeps, and trustPolicy easier to review in one place.
For a monorepo, that matters. A root workspace file can act as a security contract for every package in the repo:
packages: - 'apps/*' - 'packages/*' minimumReleaseAge: 1440 blockExoticSubdeps: true trustPolicy: no-downgrade allowBuilds: '@swc/core': true esbuild: true sharp: true
This file tells reviewers not only which packages exist in the workspace, but also which dependency behaviors are allowed.
pnpm’s catalogs push this further by centralizing dependency versions across a workspace. With strict catalog mode, pnpm add can fail when a developer tries to install a version outside the approved catalog. npm’s overrides can force versions after resolution, but catalogs are more proactive: they prevent unsupported versions from being added in the first place.
A software bill of materials, or SBOM, is a machine-readable inventory of the packages in your application. Teams use SBOMs for audits, compliance, vulnerability scanning, and incident response.
Before package managers added native SBOM support, teams often reached for tools like Syft or CycloneDX plugins. npm introduced npm sbom first, and pnpm now includes pnpm sbom as well.
That means you can generate CycloneDX or SPDX output directly from your package manager:
pnpm sbom --format cyclonedx-json > sbom.json
This is useful during incident response. When a new vulnerability or compromise is disclosed, an SBOM helps you answer a basic but urgent question: did we ship the affected package, and in which release?
pnpm 11 also implements registry operations directly. Commands such as pnpm publish, pnpm login, pnpm logout, pnpm view, pnpm deprecate, and pnpm unpublish no longer depend on the npm CLI under the hood.
This is not a feature that npm lacks; npm is still the native CLI for the npm registry. The point is that pnpm’s workflow no longer has to hand publishing and account operations off to another tool. For teams standardizing on pnpm, that makes the install-to-publish workflow more consistent.
Migrating to pnpm is usually straightforward, but the strict dependency layout can reveal hidden problems in your project.
A safe migration looks like this:
# Option 1: Install globally npm i -g pnpm # Option 2: Use Corepack corepack enable corepack use pnpm@latest # Convert the existing lockfile pnpm import # Remove the old install artifacts rm -rf node_modules package-lock.json yarn.lock # Install with pnpm pnpm install
pnpm import reads your existing package-lock.json or yarn.lock and creates a pnpm-lock.yaml that preserves your current resolutions as closely as possible. That gives you a more controlled starting point than deleting the old lockfile and resolving everything from scratch.
After the first install, expect to fix a few things:
package.jsonpnpm-workspace.yaml for monorepospnpm approve-buildsminimumReleaseAge or tune it for your release workflowpnpm consistentlyThe most common migration surprise is phantom dependencies. Code that worked under npm may fail under pnpm because it imported a package that was only available through hoisting. Treat those failures as useful signals. They show you where your dependency declarations were incomplete.
pnpm is a strong fit when your team cares about reproducible installs, monorepo performance, disk usage, and supply chain hardening. It is especially useful for:
npm is still a reasonable default for small projects, tutorials, and teams that want the least surprising path through the JavaScript ecosystem. It is preinstalled with Node.js, widely documented, and universally supported. If your project is small and your workflow is simple, npm may be enough.
But for larger applications, pnpm’s trade-offs are increasingly attractive. It keeps the familiar package.json workflow while solving several pain points that npm either leaves to configuration or exposes through years of backward-compatible behavior. Teams that also want to secure their full-stack projects from NPM attacks will find that pnpm’s stricter defaults complement those broader hardening efforts.
Package managers are now part of the security boundary. They do not just fetch dependencies; they decide which code runs during installation, how quickly new versions enter a project, how strictly dependencies are resolved, and how easily teams can audit what they shipped.
That is where pnpm has pulled ahead. Its content-addressable store reduces disk usage and repeated downloads. Its strict dependency layout exposes phantom dependencies instead of hiding them. Its workspace configuration centralizes dependency policy. And its newer security defaults make risky installation behavior harder to miss.
This does not make npm obsolete. npm created the workflow the ecosystem still builds on, and it continues to add security features of its own. The difference is that pnpm is more willing to make stricter behavior the default.
For teams maintaining large JavaScript apps or monorepos, that default posture matters. pnpm is not only faster and more disk-efficient than npm in many workflows; it is a package manager that treats dependency installation as a security-sensitive operation from the start. If you are also thinking about how your dependencies interact with authentication and access control, reviewing JWT authentication best practices is a natural next step for hardening the full application stack. And if you are adopting AI-assisted development alongside these security improvements, understanding AI-assisted development governance can help your team maintain control over what gets shipped.

Learn how to set up Meilisearch, index documents, and build keyword, semantic, and hybrid search with AI-powered retrieval.

I migrated 20 production-style components from Tailwind to StyleX. Here’s what the data showed about LOC, CSS bundle size, build time, and type safety.

A guide for using JWT authentication to prevent basic security issues while understanding the shortcomings of JWTs.

Discover how to build, render, and automate product demo videos with Remotion, replacing traditional screen recordings with reusable React code.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now