# 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: 2026-06-25
- Author: 友田 陽大
- Tags: パッケージマネージャ, モノレポ, フロントエンド, CI/CD, セキュリティ
- URL: https://tomodahinata.com/en/blog/npm-vs-yarn-vs-pnpm-package-manager-comparison-guide
- Category: Frontend
- Pillar guide: https://tomodahinata.com/en/blog/nextjs-16-app-router-cache-components-data-fetching

## Key points

- The differences among the three converge, after all, to the single point of 'how node_modules is built' — npm/Yarn Classic hoist-flatten, pnpm uses a content-addressable store + symlinks, and Yarn Modern is PnP, which doesn't create node_modules.
- Hoisting creates the hole of 'being able to import an undeclared dependency (a phantom dependency).' pnpm's non-flat structure and Yarn PnP's strict resolution block this structurally.
- pnpm hard-links the package entity to a global content-addressable store, so the same version exists on disk only once (npm in principle is a real copy per project).
- The biggest difference in 2026 is the default for supply-chain safety — pnpm since v10 (early 2025), Yarn since 4.14, don't run dependency lifecycle scripts by default. npm runs them by default in the v11 series, and opt-in is v12 (planned for summer 2026).
- Default to pnpm for new projects; npm if you need maximal compatibility and bundled-zero-config; Yarn Modern if you aim for strict boundaries or zero-installs. Whichever you choose, packageManager pinning and freeze install are mandatory.

---

"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.

| Philosophy | Representative | The form of `node_modules` |
| --- | --- | --- |
| **① Hoist-flatten** | npm / Yarn Classic | Hoist the dependency tree as flat as possible and place everything at the top level. The entity is copied per project |
| **② Content-addressable store + symlinks** | pnpm | The 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 Modern | Doesn'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.

| Viewpoint | **npm** | **Yarn Modern (2–4)** | **pnpm** |
| --- | --- | --- | --- |
| Default `node_modules` construction | hoist-flatten | PnP (no `node_modules`). `node-modules`/`pnpm` linker also selectable | non-flat, symlinks (`isolated`) |
| How the entity is held | in principle a real copy per project | zip-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`) |
| Lockfile | `package-lock.json` | `yarn.lock` (Modern is YAML) | `pnpm-lock.yaml` |
| Freeze install | `npm ci` | `yarn install --immutable` | `pnpm install --frozen-lockfile` |
| Workspaces | ✅ `workspaces` | ✅ `workspaces` + constraints | ✅ `pnpm-workspace.yaml` + **catalog** |
| Centralized dependency-version management | `overrides` | constraints / resolutions | **catalogs** (`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 series | pnpm 11 series (Node 22+) |
| Biggest strength | standard, bundled, maximal compatibility | strictness + zero-installs + extensibility | disk efficiency + strict boundaries + monorepo |
| Main weakness | safe defaults are late, disk-inefficient | PnP's tool compatibility needs an SDK | a 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.

### 2.2 pnpm: content-addressable store → virtual store → symlinks

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).

| `nodeLinker` | Behavior |
| --- | --- |
| `pnp` (default) | no `node_modules`. The strictest, fastest |
| `node-modules` | a Classic/npm-style flat `node_modules`. Compatibility first (no phantom-dependency blocking) |
| `pnpm` | uses 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).

```ts
// 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

| | npm | Yarn Modern | pnpm |
| --- | --- | --- | --- |
| Filename | `package-lock.json` | `yarn.lock` (YAML) | `pnpm-lock.yaml` (YAML) |
| Integrity | records SHA-512 SRI strings | records resolution checksums | records the resolution |
| Freeze | `npm 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.**"

| Feature | npm | Yarn Modern | pnpm |
| --- | --- | --- | --- |
| Workspace definition | `workspaces` in `package.json` | `workspaces` in `package.json` | `pnpm-workspace.yaml` |
| Cross-cutting execution | `--workspaces` / `--workspace` | `yarn workspaces foreach` (parallel, topological order) | `pnpm -r` / `--filter` |
| Centralized version management | `overrides` (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.

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

catalog:
  react: ^19.2.0
  zod: ^3.25.0
```

```jsonc
// 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](/blog/pnpm-turborepo-monorepo-architecture-type-coverage-guide). The knowledge from a real project (the 14-shared-package configuration of the [subscription learning platform](/case-studies/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)

| Tool | Run a dependency's install/postinstall by default… | Introduction version / opt-in means |
| --- | --- | --- |
| **pnpm** | **don't run** | from **v10** (early 2025). Allow with `onlyBuiltDependencies` / `pnpm approve-builds`. Consolidated into `allowBuilds` in v11 |
| **Yarn Modern** | **don't run** | from **4.14**. Allow all with `enableScripts: true`, individually with `dependenciesMeta` |
| **npm** | **run** | the 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.

```yaml
# pnpm-workspace.yaml — ビルドを許可する依存だけを明示（v10+ は既定でブロック）
onlyBuiltDependencies:
  - esbuild
  - "@swc/core"
# ↑ ここに無い依存の postinstall は実行されない。`pnpm approve-builds` で対話的に承認も可
```

```yaml
# .yarnrc.yml — Yarn 4.14+ は既定でスクリプト無効。必要な依存だけ個別に許可するのが筋
# enableScripts: false   # ← 既定。全体ONは avoid
```

```ini
# .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.

```jsonc
// package.json — チーム/CI で同一の PM バージョンを強制
{
  "packageManager": "pnpm@11.0.0"
}
```

```bash
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.
