Skip to main content
友田 陽大
Frontend
パッケージマネージャ
モノレポ
フロントエンド
CI/CD
セキュリティ

npm vs Yarn vs pnpm thorough comparison: a package-manager selection guide read through the official docs [2026 edition]

An accurate technical comparison of the differences between npm, Yarn, and pnpm based on each official documentation (as of June 2026). From the node_modules construction method, the blocking of phantom dependencies, disk efficiency, lockfiles, and monorepos, to the 'dependency scripts off by default' that works against supply-chain attacks, it systematizes selection criteria from the design philosophy.

Published
Reading time
19 min read
Author
友田 陽大
Share

"Is npm fine, should I go with Yarn, or pnpm?" — right after the first git init of a new project, it's a question you almost always stall on. And many teams decide it by preference or past inertia.

Let me state the conclusion first. If you start a new project in 2026, defaulting to pnpm is reasonable. With disk efficiency, strict dependency boundaries, centralized dependency management for monorepos, and the default for supply-chain safety, it's currently the best-balanced. But this isn't a story of "pnpm is justice." If some tools presuppose a flat node_modules, then npm; if you aim for ultimate strictness and zero-installs, then Yarn Modern (PnP) — the correct answer changes by requirements.

What's important is not to talk about the three's differences with impressions like "fast/slow" or "trendy/obsolete." The differences among the three, pushed to the limit, converge to the single design philosophy of 'how to build node_modules.' If you understand this one point, you can deductively explain everything — disk efficiency, the safety of phantom dependencies, CI speed, and tool compatibility. This article decomposes it, faithful to each tool's official documentation.

The rules of this article: specs, defaults, and config keys are based on the descriptions of npm official (docs.npmjs.com) / Yarn official (yarnpkg.com, classic.yarnpkg.com) / pnpm official (pnpm.io) as of June 2026. A package manager's defaults change by version (the "dependency scripts off by default" described later was introduced in different versions: pnpm v10, Yarn 4.14, npm v12). Always confirm in the official documentation of the version you use before production. No secrets are included in the configuration examples (on the premise of environment variables; hardcoding is strictly forbidden).


0. Mental model: everything is decided by "how node_modules is made"

First, let me fix just one axis running through this article.

The essential difference among the three = "how to build node_modules (= the place where Node.js resolves dependencies at runtime)."

Node.js's module resolution is simple — on each require/import, it just traverses node_modules upward from the current directory. A package manager's job is to prepare node_modules (or its alternative) so that this resolution happens correctly and fast. Here are three philosophies.

PhilosophyRepresentativeThe form of node_modules
① Hoist-flattennpm / Yarn ClassicHoist the dependency tree as flat as possible and place everything at the top level. The entity is copied per project
② Content-addressable store + symlinkspnpmThe entity is hard-linked to a single global store. node_modules is non-flat and symlinks only the declared dependencies
③ Don't make node_modules (PnP)Yarn ModernDoesn't generate node_modules, but supplies dependencies with a resolution map called .pnp.cjs and a zip cache

This choice of ①②③ decides all of the following.

  • Disk usage (copy the entity, or share it)
  • The safety of phantom dependencies (can you import an undeclared dependency)
  • Install speed (the amount of copying and the link strategy)
  • Tool compatibility (do tools that physically expect node_modules work)

The subsequent chapters dig into the specifics along this axis.


1. Conclusion in advance: the overall comparison matrix

Before getting into details, let me show the big picture in one sheet. The basis of each cell is stated in the subsequent chapters.

ViewpointnpmYarn Modern (2–4)pnpm
Default node_modules constructionhoist-flattenPnP (no node_modules). node-modules/pnpm linker also selectablenon-flat, symlinks (isolated)
How the entity is heldin principle a real copy per projectzip-compressed cache (.yarn/cache)hard link to the global content-addressable store (1 version = 1 disk copy)
Blocking phantom dependencies❌ passes through by default✅ PnP strictly blocks✅ blocks by default (isolated)
Lockfilepackage-lock.jsonyarn.lock (Modern is YAML)pnpm-lock.yaml
Freeze installnpm ciyarn install --immutablepnpm install --frozen-lockfile
Workspacesworkspacesworkspaces + constraintspnpm-workspace.yaml + catalog
Centralized dependency-version managementoverridesconstraints / resolutionscatalogs (catalog:)
Default for dependency scripts⚠️ runs (v11 series. opt-in planned in v12)off by default (4.14+)off by default (v10+)
Suppressing install just after publish(no standard setting)npmMinimalAgeGate (4.12+, default 1 day)minimumReleaseAge (v11 default 1 day)
Current major (2026-06)npm 11 series (bundled with Node)Yarn 4 seriespnpm 11 series (Node 22+)
Biggest strengthstandard, bundled, maximal compatibilitystrictness + zero-installs + extensibilitydisk efficiency + strict boundaries + monorepo
Main weaknesssafe defaults are late, disk-inefficientPnP's tool compatibility needs an SDKa rare case of getting stuck with symlink-unsupported tools

About Yarn Classic (1.x): the current latest is the 1.22 series, and the official positions it as "maintenance mode (no new features, only critical/security fixes)." The target of new adoption is effectively Yarn Modern (2+). When this article simply writes "Yarn," it refers to Modern.


2. Install-model deep dive: this is how node_modules is made

2.1 npm: "maximal flattening" by hoisting

npm, since v3, makes node_modules as flat as possible. In the official expression, "dependencies are placed at the highest possible level (maximally flat)," and the rule is — "if an identical package already exists in an ancestor's node_modules, don't put it at the current level."

But since version conflicts can't be flattened, nesting remains as needed. The official example:

node_modules/
├── a/                      # a@1
├── b/                      # b uses a@1 (the top a suffices)
└── c/
    └── node_modules/
        └── a/              # c uses a@2 → nested only here

Only when a different version like a@1 and a@2 is needed, c/node_modules/a is dug. This is the reality of "maximal flattening (hoisting)." Combined with the resolver's nature of traversing upward, even a nested c can reach the top-level dependency.

pnpm's structure is three stages.

  1. Content-addressable store: store all files of all packages in a single place on disk only once (on macOS, the default is ~/Library/pnpm/store).
  2. Virtual store node_modules/.pnpm: each package is expanded here in the form <name>@<version>/node_modules/<name>, and the entity is a hard link from the store.
  3. Symlinks: in the project's top-level node_modules, only the direct dependencies declared in package.json are lined up as symlinks into .pnpm.

The structure example in the official documentation (when foo depends on bar):

node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo      # only direct dependencies appear at the top
└── .pnpm
    ├── bar@1.0.0/node_modules/bar -> <store>       # the entity is a hard link to the store
    └── foo@1.0.0/node_modules
        ├── foo -> <store>
        └── bar -> ../../bar@1.0.0/node_modules/bar # transitive deps are relative-linked within .pnpm

The point is the single fact "only the declared dependencies appear at the top." This directly ties to the next chapter's blocking of phantom dependencies.

2.3 Yarn Modern: don't make node_modules (PnP)

Yarn Modern's default linker is PnP (Plug'n'Play), which doesn't generate node_modules in the first place. Instead, it generates .pnp.cjs. This is, in the official words, a loader that "has all the information of the project's dependency tree, and teaches the tools where each package is on disk and how to resolve require/import." The dependency entities are placed as zip archives inside .yarn/cache and referenced directly via the loader.

PnP's benefit is strictness. The official clearly states "because Yarn has a list of all packages and their dependencies, it can prevent access to a dependency not accounted for at resolution time." = phantom dependencies become structurally impossible.

Note that Yarn Modern doesn't force PnP. You can switch with the nodeLinker setting (the three values the official deems stable).

nodeLinkerBehavior
pnp (default)no node_modules. The strictest, fastest
node-modulesa Classic/npm-style flat node_modules. Compatibility first (no phantom-dependency blocking)
pnpmuses a content-addressable store with symlinks + hard links (pnpm-style)

A common misspelling: nodeLinker: pnp-loose doesn't exist. Strict/loose is controlled by a separate setting pnpMode (strict = default / loose).


3. Phantom dependencies: the quiet landmine hoisting creates

This is the point where accidents occur most quietly in real projects.

3.1 What happens

In hoist-flattening (npm / Yarn Classic / Yarn's node_modules linker), since transitive dependencies are hoisted to the top level, the Node resolver finds them. As a result, you can import a package not declared in package.json. This is a phantom dependency (a ghost dependency).

// package.json には "express" しか書いていない。
// しかし express は内部で "debug" に依存しており、debug がトップに巻き上がる。

import createDebug from "debug"; // ✅ 動いて「しまう」
const log = createDebug("app");

This code works today. But —

  • What if express replaces debug with something else in the future? → One day, suddenly Cannot find module 'debug'.
  • What if the hoisting position changes on a machine/CI with a different dependency configuration? → "It works in my environment but breaks in production."

npm official clearly states hoisting and the "resolver traverses upward" behavior but doesn't warn about this as a "phantom dependency" risk. It's a pitfall widely known as a consequence of the mechanism.

3.2 pnpm / Yarn PnP kill this structurally

  • pnpm (default isolated): only the declared dependencies appear in the top-level node_modules. In the official words, "only packages that are genuinely in dependencies are accessible." An undeclared import can't be resolved and fails at development time.
  • Yarn PnP: rejects access to a dependency not in the resolution map with a meaningful error.

Practical implication: "migrating an existing repo that has run for years on the hoisting premise to pnpm / PnP exposes the hidden phantom dependencies all at once" — this is not a bug but the visualization of debt that has been hidden until now. Take the migration as "it became correct," not "it broke," and correctly declare the exposed dependencies in package.json.


4. Disk efficiency and speed: why pnpm is "light"

4.1 How the entity is held is fundamentally different

  • npm: it has a global cache (_cacache under ~/.npm, a content-addressable method with integrity verification), but at install time it expands the real files into each project's node_modules. = if you use the same react@19 in 10 projects, in principle 10 copies (the experimental linked strategy is an exception).
  • Yarn Modern: holds dependencies as content-addressable zip archives in the global cache. When using the node-modules linker, cross-project hard-link sharing is also possible with nmMode: hardlinks-global.
  • pnpm: one version exists on disk only once. Since each project's entity is a hard link to the store, it "consumes no additional disk" (official). Furthermore, updates are also differential — even if you bump to a version where only 1 of 100 files changed, "only 1 file is added to the store."

The numbers swing greatly by environment (OS, file system, cache warmth, dependency scale), so this article doesn't assert the order of magnitude. What's important is the structural reason that "pnpm's disk advantage isn't a coincidence but a necessity of the design of a content-addressable store + hard links." Install speed similarly benefits from the structure of "minimize the amount of real-file copying and get by with links."

4.2 Cleaning the cache

  • pnpm: pnpm store prune (removes entities not referenced by any project. No side effect since they're re-fetched).
  • npm: npm cache verify (integrity verification) / clean only when needed for capacity recovery.
  • Yarn: the global cache is controlled by enableGlobalCache (default true).

5. Lockfiles and reproducibility: practices that don't break CI

npmYarn Modernpnpm
Filenamepackage-lock.jsonyarn.lock (YAML)pnpm-lock.yaml (YAML)
Integrityrecords SHA-512 SRI stringsrecords resolution checksumsrecords the resolution
Freezenpm ci (lock required, deletes node_modules and rebuilds, fails if inconsistent with package.json)yarn install --immutable (forbids lock updates)pnpm install --frozen-lockfile

The two iron rules common to the three are:

  1. Always commit the lockfile (all three tools officially clearly state this). The single source of truth that guarantees the same tree in dev, CI, and production.
  2. Use freeze install in CI. Using a command that "can update the lock," equivalent to npm install, in CI is the act of discarding reproducibility yourself. CI uses the freeze series in the table above and stops lock drift with a red light.

In building payment platforms and an award-winning B2B SaaS, I've seen that the majority of "it passes locally but fails in CI / breaks only in production" stems from install non-determinism. Just placing npm ci / --immutable / --frozen-lockfile at the CI gate can structurally erase this kind of accident.


6. Monorepo: workspaces and "centralized dependency management"

All three natively support workspaces (monorepos). The workspace: protocol (referencing an internal package locally and converting it to the real version only at publish) is also common. The difference appears in "how to gather the versions of external dependencies in one place."

FeaturenpmYarn Modernpnpm
Workspace definitionworkspaces in package.jsonworkspaces in package.jsonpnpm-workspace.yaml
Cross-cutting execution--workspaces / --workspaceyarn workspaces foreach (parallel, topological order)pnpm -r / --filter
Centralized version managementoverrides (forced override)constraints (declare conventions in JS, --fix possible)catalogs (shared constants with catalog:)
Carving out distribution artifacts(manual)(manual / plugin)pnpm deploy (generates a self-contained node_modules. Docker-oriented)

pnpm's catalog is a mechanism that defines the version of a shared external dependency like zod or react in one place in pnpm-workspace.yaml and references it from each package.json with "react": "catalog:". It structurally kills the silent drift of versions — one of the most effective features in a monorepo.

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

catalog:
  react: ^19.2.0
  zod: ^3.25.0
// apps/web/package.json — バージョンは catalog に委譲(ここでは固定しない)
{
  "dependencies": {
    "react": "catalog:",
    "zod": "catalog:"
  }
}

The design to make a monorepo "a growable asset" (erase drift with catalog, cache with Turborepo, make shared domain types the single source of truth) is dug into with real code in a separate article: designing a type-safe monorepo with pnpm + Turborepo. The knowledge from a real project (the 14-shared-package configuration of the subscription learning platform) is the foundation.


7. Supply-chain safety: in 2026, the biggest difference is here

This is the most practically-meaningful difference in 2026, and a point both the client and the contractor should look at. The key is "whether to run a dependency package's lifecycle scripts (postinstall, etc.) by default." This is because it's a major route of supply-chain attacks where third-party code is arbitrarily executed just by npm install.

7.1 The default for dependency scripts (strict by version)

ToolRun a dependency's install/postinstall by default…Introduction version / opt-in means
pnpmdon't runfrom v10 (early 2025). Allow with onlyBuiltDependencies / pnpm approve-builds. Consolidated into allowBuilds in v11
Yarn Moderndon't runfrom 4.14. Allow all with enableScripts: true, individually with dependenciesMeta
npmrunthe v11 series runs by default. To opt-in in v12 (planned for summer 2026). A preview npm approve-scripts in 11.16+

In other words, as of June 2026, pnpm and Yarn are "safe by default," npm is "still runs by default" (expected to follow in v12). This difference works as a difference in the default stance toward enterprise security requirements.

# pnpm-workspace.yaml — ビルドを許可する依存だけを明示(v10+ は既定でブロック)
onlyBuiltDependencies:
  - esbuild
  - "@swc/core"
# ↑ ここに無い依存の postinstall は実行されない。`pnpm approve-builds` で対話的に承認も可
# .yarnrc.yml — Yarn 4.14+ は既定でスクリプト無効。必要な依存だけ個別に許可するのが筋
# enableScripts: false   # ← 既定。全体ONは avoid
# .npmrc — npm を今すぐ硬めるなら(注意:これは“自分のスクリプトも”止める)
# ignore-scripts=true
# → CI では `npm ci --ignore-scripts` 後、必要なビルドだけ明示実行するのが安全。
#   npm 11.16+ なら `npm approve-scripts` による選択的許可が望ましい。

7.2 The "age gate" that doesn't step on a malicious release just after publish

Recent attacks increasingly take the type of "hijack a legitimate package and spread a malicious version just after publish." Against this:

  • pnpm: minimumReleaseAge (v11 default 1 day) — don't install a version that hasn't passed N minutes since publish.
  • Yarn: npmMinimalAgeGate (4.12+, default 1 day) — the same gist.
  • npm: no standard equivalent setting (as of 2026-06).

7.3 Integrity, provenance, audit

  • Integrity: npm holds SHA-512 SRI in the lock, Yarn/pnpm hold checksums. Yarn's hardened mode (enableHardenedMode) cross-checks the lock content against the registry to detect tampering, and is automatically enabled on a PR from a public repository.
  • Provenance: npm provides provenance attestation by sigstore (npm publish --provenance, needs id-token: write in GitHub Actions) and verification by npm audit signatures.
  • Audit: npm audit / yarn npm audit / pnpm (pnpm audit). None of them auto-run at install time, so incorporating them into an independent CI job is the standard.

When asked "how do you do security" in the contracting field, being able to immediately answer "I stop dependency scripts by default, enable the age gate, and put freeze install and audit at the CI gate" directly becomes a difference in trust. What I've thoroughly done in a B2B SaaS that passed 4 rounds of security audits, and a payment platform with 0 double charges in production, is exactly this design of "enforce safety not by operational carefulness but by the tool's defaults and CI's red light."


8. Version pinning: packageManager and Corepack

A version difference in the tool itself, like "local is pnpm 9, CI is pnpm 11," is also a factor that breaks reproducibility. All three can be pinned with the packageManager field of package.json, and Corepack reads it and automatically uses the corresponding version.

// package.json — チーム/CI で同一の PM バージョンを強制
{
  "packageManager": "pnpm@11.0.0"
}
corepack enable          # packageManager の宣言に従って pm を解決

Note (2026): Corepack was bundled with the Node 14.19–24 series, but from Node 25 onward it's removed from the bundle. On Node 25+, introduce it separately with npm i -g corepack. The packageManager declaration itself remains valid.


9. Selection framework: work backward from requirements

Select from requirements, not "because it's fast" or "because it's trendy."

If you have these requirements → pnpm (the default for new)

  • Emphasize disk efficiency (CI runners, dev machines, many projects).
  • Want to structurally forbid phantom dependencies (strict dependency boundaries).
  • Want to centrally manage dependency versions in a monorepo (catalog).
  • Want to secure supply-chain safety by default (scripts off by default + age gate).

If you have these requirements → npm

  • Need no additional install and maximal compatibility (bundled with Node. Minimal friction in CI, education, OSS distribution).
  • Some tools physically presuppose a flat node_modules.
  • Want to minimize the team's learning cost (the most mature standard).

If you have these requirements → Yarn Modern

  • Aim for ultimate strictness (PnP) and zero-installs (commit .yarn/cache and erase yarn install itself).
  • Want to declaratively enforce workspace conventions with constraints.
  • Can tolerate the operational cost of introducing an editor SDK (yarn dlx @yarnpkg/sdks) for PnP.

What about Yarn Classic (1.x)?

New adoption is not recommended. The official positions it as maintenance mode, and there's little reason to choose it other than maintaining an existing repo. Realistic migration targets are pnpm or Yarn Modern.

Mandatory regardless of which you choose: pin the tool version with packageManager, commit the lockfile, and use freeze install in CI (npm ci / yarn install --immutable / pnpm install --frozen-lockfile). Don't mix multiple lockfiles in one repository (a hotbed of accidents).


10. Mistakes contractors and clients tend to step on (practical checklist)

  • Mixing lockfiles: package-lock.json and pnpm-lock.yaml coexist → which is the source of truth is undefined. Unify to one and delete the unnecessary.
  • npm install in CI: non-deterministic since it can update the lock. Unify to the freeze series.
  • Running dependency scripts defenselessly: allowing all with the npm default or enableScripts: true/dangerously-allow-all-builds is dangerous from a supply-chain standpoint. Narrow allowance to only the necessary dependencies.
  • packageManager not pinned: the tool version splits between local and CI, and reproducibility collapses.
  • Leaving phantom dependencies in a pnpm/PnP migration: "just hide the exposed undeclared dependencies with shamefully-hoist/the node-modules linker for now" is deferring debt. Correctly declare them in package.json.
  • Habitual use of shamefully-hoist: it's a last resort for compatibility, not a default operation.

FAQ

Q. Is npm already old? No. npm is the standard bundled with Node and has the clear strength of maximal compatibility. Its safe defaults (script opt-in) are late, but it's expected to follow in v12 (planned for summer 2026). In scenes where "a mature standard is needed," it can still be the first candidate.

Q. Can pnpm be used safely in production? Yes. It's used in many production monorepos, and rather leads in safe defaults (scripts off by default since v10, the age gate of v11) and disk efficiency. The caveat is the rare getting-stuck with some symlink-unsupported tools, in which case alone you deal with it with node-linker: hoisted, etc.

Q. Why hasn't Yarn PnP fully spread? The reasons are the friction with some tools that physically presuppose node_modules, and the operational cost of needing an SDK introduction for editor completion. It strongly fits an organization where the value of strictness and zero-installs exceeds the operational cost. If compatibility is emphasized, you can also choose nodeLinker: node-modules.

Q. Should I switch an existing project? If it works and you have no complaints, there's no need to rush. The biggest motives for switching are "disk/CI efficiency," "eradicating phantom dependencies," and "safe defaults." When migrating, take the exposure of phantom dependencies as the visualization of debt, and switch only after definitely putting freeze install into CI.

Q. In the end, which should the first one be? For new projects in 2026, default to pnpm. If maximal compatibility is a requirement, npm; if you actively go for strictness and zero-installs, Yarn Modern. Whichever you choose, packageManager pinning and freeze install come as a set.


Summary

  • The differences among the three consolidate to the single point of "how to make node_modules" — npm/Yarn Classic hoist-flatten, pnpm uses a content-addressable store + symlinks, Yarn Modern is PnP.
  • Hoisting creates the quiet landmine of phantom dependencies. pnpm's non-flat structure and Yarn PnP's strict resolution block this structurally.
  • Disk efficiency is a design necessity of pnpm (1 version = 1 disk copy).
  • The biggest difference in 2026 is the default for supply-chain safety — pnpm (from v10) and Yarn (from 4.14) stop dependency scripts by default, and npm is expected to follow in v12.
  • Select the tool by requirements. But packageManager pinning, committing the lock, and CI freeze install are common mandatory practices regardless of which you choose.

Package-manager selection looks like a small judgment, but it directly ties to the foundations of production operation: disk/CI cost, the safe boundary of dependencies, and the default stance of the supply chain. I've built an award-winning B2B SaaS and payment platforms fast, cheap, and safe with "one person × generative AI." Behind that foundation is always this design of "enforce correctness not by operational good faith but by the tool's defaults and CI's red light." If you're troubled by technology selection or building production quality, please feel free to consult me.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading