| |

Yarn 01 🧶 Introduction to Yarn vs npm

⏱️ Estimated read: 13 min | |

The JavaScript ecosystem has never had a single package manager. For years, npm was the default—it shipped with Node.js, and it worked. But in 2016, Yarn arrived with a promise of speed, determinism, and better dependency management. Nearly a decade later, the landscape is more complicated: Yarn has evolved through multiple major versions, npm has closed many of its gaps, and the choice between them now depends on what you value most. This chapter covers the fundamental differences between Yarn and npm, the trade-offs each makes, and the specific scenarios where one clearly outperforms the other.

Key point: Yarn and npm are not interchangeable tools with different logos. They embody different philosophies about dependency resolution, disk usage, and developer workflow—and choosing the wrong one for your project can introduce subtle bugs or massive maintenance overhead.


Why Yarn exists (and why npm still matters)

The original speed problem. When Yarn launched in 2016, npm install was notoriously slow. It resolved dependencies sequentially, downloaded packages one at a time, and had no offline cache. Yarn introduced parallel downloads, a global cache, and lockfiles that guaranteed identical installs across machines. For a few years, Yarn was objectively faster and more reliable .

The determinism problem. npm’s early lockfiles were fragile. Yarn’s yarn.lock was designed from the start to pin every transitive dependency to an exact version and integrity hash. This meant that two developers running yarn install on the same project would get byte-identical dependency trees. npm eventually adopted the same idea with package-lock.json, but Yarn set the standard .

The ghost dependency problem. npm uses a flat node_modules layout. When you install Package A, its dependencies get hoisted to the top level of node_modules. Your code can then import Package B—a transitive dependency you never declared—and it will work. This is a “ghost dependency,” and it causes production failures when Package A updates and drops Package B. Yarn’s Plug’n’Play (PnP) mode eliminates this entirely by enforcing that your code can only import what you explicitly declared .

The npm counterattack. npm did not stand still. Version 5 introduced package-lock.json, version 7 added workspaces, and versions 9 through 11 brought significant performance improvements and security features like trusted publishing via OIDC . Today, npm is no longer the slow, unreliable tool it once was. It remains the universal default—bundled with Node.js, supported by every tool, and requiring zero setup .

The trade-off. Yarn offers stricter dependency enforcement and faster installs in PnP mode. npm offers universal compatibility and zero configuration. The right choice depends on whether you value correctness over convenience, or convenience over correctness.


a. Dependency resolution: flat hoisting vs Plug’n’Play

The most fundamental difference between npm and Yarn is how they place packages on disk. This difference determines what your code can import, how fast installs run, and what breaks when things go wrong.

npm uses a flat, hoisted node_modules layout. When you run npm install, npm downloads every package and its transitive dependencies, then flattens them into a single node_modules directory. If Package A depends on lodash@4.17.20 and Package B depends on lodash@4.17.21, npm picks one version (usually the newer one) and places it at the top level. Both A and B will use that single copy.

node_modules/
├── lodash/          (hoisted, shared)
├── package-a/       (declared in your package.json)
└── package-b/       (declared in your package.json)

This layout has a critical consequence: your code can import anything that exists in node_modules, even if you never declared it. If package-a depends on axios, your code can require('axios') without adding it to your package.json. This works locally, but it breaks when package-a updates and drops axios as a dependency. The fix is to declare axios explicitly—but many developers never discover the problem until production breaks.

Yarn’s default mode in modern versions is Plug’n’Play (PnP). Instead of creating a node_modules directory, Yarn generates a .pnp.cjs file that maps every package to its exact location on disk. Node.js loads this file at startup, and when your code calls require('lodash'), the loader checks whether lodash is declared in your package.json. If it is not, the import fails immediately.

.pnp.cjs          (dependency map)
.yarn/
├── cache/        (compressed package archives)
└── releases/     (Yarn binary)

The benefit is strictness. You cannot accidentally import a package you never declared. The cost is compatibility: some packages assume the existence of node_modules and break under PnP . Yarn allows you to fall back to node_modules mode if needed, but doing so sacrifices PnP’s strictness.


b. Installation speed and disk usage

Yarn’s PnP mode avoids writing files to disk entirely. Packages are stored as compressed archives in .yarn/cache, and the loader resolves imports directly from those archives. For a project with hundreds of dependencies, this can reduce install times from tens of seconds to a few seconds .

npm, by contrast, copies every package into node_modules. A project with 400 transitive dependencies might create 50,000 individual files on disk. On macOS and Windows, where file system operations are relatively slow, this file copying dominates install time. On Linux, it is faster but still measurable.

The disk usage difference is equally stark. npm stores a separate copy of every package version in every project’s node_modules. If you work on five projects that all use React 19, npm stores five copies of React on your disk. Yarn’s PnP cache is shared across projects, so React is stored once and referenced by every project that needs it .

Benchmarks published by Yarn’s team show PnP installs completing in a fraction of npm’s time, particularly on warm caches where npm still has to verify and copy files . Independent benchmarks corroborate this: npm cold installs often take 14 seconds or more for a mid-sized project, while Yarn PnP can complete the same install in 3 to 5 seconds .

The caveat is that PnP requires ecosystem compatibility. Some tools—particularly older build tools, test runners, and IDE plugins—assume node_modules exists and fail under PnP. Yarn maintains a compatibility list, and the ecosystem has improved significantly since PnP’s rocky introduction, but the risk of encountering a broken package remains .


c. Workspaces, monorepos, and advanced tooling

Both npm and Yarn support workspaces—the ability to manage multiple packages within a single repository. But their implementations differ in maturity and feature depth.

npm added workspaces in version 7 (2021). The implementation is functional but basic: you declare workspace paths in your root package.json, and npm links them together during install. npm does not offer workspace-level dependency constraints, task runners, or shared dependency catalogs .

Yarn’s workspace support is significantly more developed. Beyond basic linking, Yarn offers constraints (a system for linting your entire dependency tree against custom rules), workspace profiles (shared sets of dev dependencies across packages), the catalog: protocol (unified dependency versions across workspaces), and a built-in task runner that understands cross-workspace dependencies . If you are managing a monorepo with dozens of packages, these features eliminate entire categories of configuration and tooling overhead.

Yarn also includes features that npm requires separate tools for: dependency patching (yarn patch), Node.js version management (yarn switch), and a plugin system for extending the package manager’s behavior. npm’s philosophy is to do one thing well and leave the rest to the ecosystem. Yarn’s philosophy is to provide a complete toolchain .


Complete Example Session

# ============================================
# PART 1: INITIALIZING A PROJECT
# ============================================

# npm: zero setup, bundled with Node.js
npm init -y

# Yarn: requires Corepack (bundled with Node.js 16.10+)
corepack enable
yarn init -2
# ============================================
# PART 2: INSTALLING DEPENDENCIES
# ============================================

# npm: installs into node_modules/
npm install express

# Yarn: installs into .yarn/cache/ (PnP mode)
yarn add express
# ============================================
# PART 3: THE LOCKFILE
# ============================================

# npm: package-lock.json
# Generated automatically, pins exact versions

# Yarn: yarn.lock
# Generated automatically, pins exact versions
# Plus .pnp.cjs for dependency resolution
# ============================================
# PART 4: DEPENDENCY STRICTNESS
# ============================================

# npm: ghost imports work
# (axios is a dependency of express, not your project)
node -e "require('axios')"   # Works if express depends on it

# Yarn PnP: ghost imports fail
node -e "require('axios')"   # Error: axios not declared
# ============================================
# PART 5: INSTALL SPEED
# ============================================

# npm: cold install (no cache)
time npm install    # ~14s for mid-sized project

# Yarn PnP: cold install
time yarn install   # ~3s for same project
# ============================================
# PART 6: DISK USAGE
# ============================================

# npm: copies into each project
du -sh node_modules/   # ~487 MB for 200 deps

# Yarn PnP: compressed cache
du -sh .yarn/cache/    # ~150 MB for same deps
# ============================================
# PART 7: FALLING BACK TO NODE_MODULES
# ============================================

# If a package breaks under PnP:
yarn config set nodeLinker node-modules
yarn install

# Now node_modules exists, but you lose PnP's strictness
# ============================================
# PART 8: WORKSPACES
# ============================================

# npm: basic workspaces
# package.json at root:
{
  "workspaces": ["packages/*"]
}

# Yarn: extended workspaces
# package.json at root:
{
  "workspaces": ["packages/*"]
}
# Plus constraints, profiles, catalogs
# ============================================
# PART 9: VERSION PINNING
# ============================================

# npm: uses the Node.js bundled version
npm --version   # e.g., 11.2.0

# Yarn: pins per-project via packageManager field
# package.json:
{
  "packageManager": "yarn@4.5.0"
}

The nine parts above demonstrate the core differences: initialization, dependency installation, lockfile generation, strictness enforcement, speed, disk usage, fallback options, workspace support, and version management. Each part highlights a concrete decision point where Yarn and npm diverge.


Quick Reference

npm vs Yarn at a Glance

FeaturenpmYarn
Ships with Node.jsYesNo (via Corepack)
Default layoutFlat node_modulesPnP (no node_modules)
Ghost dependency protectionNoYes (PnP mode)
Lockfilepackage-lock.jsonyarn.lock
WorkspacesBasic (since v7)Advanced (constraints, profiles)
Dependency patchingNoYes (yarn patch)
Task runnerNoYes
Node version managementNoYes (yarn switch)

When to Choose npm

ScenarioReason
Maximum compatibilityEvery tool supports npm
Zero configurationAlready installed with Node.js
Simple projectsComplexity not justified
Legacy codebasesMigration cost rarely worth it
Open-source contributionContributors expect npm

When to Choose Yarn

ScenarioReason
Strict dependency enforcementPnP eliminates ghost imports
Monorepo managementConstraints and profiles
Maximum install speedPnP avoids disk writes
Zero-install workflowsCommit .yarn/cache to git
Team standardizationPin exact version via packageManager

Best Practices

✅ Do This:

npm install express
<!-- Declares dependency explicitly -->          # ✅
yarn add express
<!-- PnP enforces declared dependencies -->     # ✅
# Commit package-lock.json or yarn.lock
<!-- Pins transitive dependencies -->            # ✅
# npm: use workspaces for monorepos
<!-- Basic but functional -->                    # ✅
# Yarn: enable Corepack for version pinning
corepack enable                                  # ✅
# Test PnP compatibility before migrating
yarn install --immutable                         # ✅

❌ Don’t Do This:

# npm: rely on ghost imports
require('axios')  # Not in package.json           # ❌
# Yarn PnP: assume node_modules exists
ls node_modules/  # Directory doesn't exist       # ❌
# Mix package managers in one project
npm install && yarn add lodash                    # ❌
# Commit node_modules or .yarn/cache without thinking
<!-- Large, unnecessary git history -->           # ❌
# Ignore lockfile conflicts
<!-- Resolve manually, never delete -->           # ❌
# Migrate Yarn Classic to Berry without testing
<!-- PnP breaks some packages -->                 # ❌

Common Pitfalls

PitfallWhy It HappensFix
Ghost imports break in productionnpm’s flat layout allows undeclared importsDeclare all dependencies explicitly
PnP breaks a build toolTool assumes node_modules existsFall back to nodeLinker: node-modules
Yarn not installed on CICI uses npm by defaultEnable Corepack or install Yarn explicitly
Lockfile conflicts in gitMultiple developers edit dependenciesResolve manually; regenerate lockfile
Migration breaks peer dependenciesYarn enforces strict peer depsAdd overrides or use npm
Disk space wastednpm copies packages per projectUse Yarn PnP or pnpm for shared cache

Real-World Examples

1. Initializing a Yarn Project

corepack enable
yarn init -2

2. Installing with npm

npm install express mongoose

3. Installing with Yarn PnP

yarn add express mongoose

4. Checking Ghost Dependencies (npm)

// This may work even if axios is not in package.json
const axios = require('axios');

5. Strict Import (Yarn PnP)

// This fails unless axios is declared
const axios = require('axios');

6. Falling Back to node_modules

yarn config set nodeLinker node-modules
yarn install

7. Pinning Yarn Version

{
  "packageManager": "yarn@4.5.0"
}

8. Workspace Configuration

{
  "workspaces": ["packages/*"]
}

9. CI with Corepack

corepack enable
yarn install --immutable

10. Checking Install Time

time npm install
time yarn install

Visual

npm’s Flat node_modules Layout

┌──────────────────────────────────────────────┐
│  Your Project                                │
│                                              │
│  node_modules/                               │
│  ├── express/                                │
│  │   └── (depends on axios)                  │
│  ├── axios/       ← hoisted to top level     │
│  ├── lodash/      ← hoisted from somewhere   │
│  └── your-code/                              │
│                                              │
│  Your code can import axios, lodash,         │
│  and anything else in node_modules—          │
│  even if you never declared it.              │
└──────────────────────────────────────────────┘

The flat layout is convenient but permissive. Ghost dependencies hide until a transitive dependency changes.

Yarn PnP’s Strict Resolution

┌──────────────────────────────────────────────┐
│  Your Project                                │
│                                              │
│  .pnp.cjs          (dependency map)          │
│  .yarn/                                      │
│  └── cache/        (compressed archives)     │
│                                              │
│  Your code calls require('express')          │
│  Loader checks package.json: declared?       │
│  ├── Yes → resolve from cache                │
│  └── No  → throw error immediately           │
│                                              │
│  No node_modules directory exists.           │
└──────────────────────────────────────────────┘

PnP enforces discipline at the cost of compatibility. The error is immediate and explicit, not deferred until production.

Disk Usage Comparison

┌──────────────────────────────────────────────┐
│  npm: 5 projects × 400 deps each             │
│                                              │
│  Project 1 → node_modules (487 MB)           │
│  Project 2 → node_modules (487 MB)           │
│  Project 3 → node_modules (487 MB)           │
│  Project 4 → node_modules (487 MB)           │
│  Project 5 → node_modules (487 MB)           │
│                                              │
│  Total: ~2.4 GB                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Yarn PnP: 5 projects, shared cache          │
│                                              │
│  ~/.yarn/berry/cache/ (150 MB, shared)       │
│                                              │
│  Project 1 → links to cache                  │
│  Project 2 → links to cache                  │
│  Project 3 → links to cache                  │
│  Project 4 → links to cache                  │
│  Project 5 → links to cache                  │
│                                              │
│  Total: ~150 MB                              │
└──────────────────────────────────────────────┘

The shared cache is Yarn’s most tangible benefit for developers working across multiple projects.


Summary

ItemValue
npm default layoutFlat node_modules
Yarn default layoutPlug’n’Play (no node_modules)
npm lockfilepackage-lock.json
Yarn lockfileyarn.lock
npm workspacesBasic, since v7
Yarn workspacesAdvanced: constraints, profiles, catalogs
npm install speedBaseline
Yarn PnP install speed2–4× faster
npm disk usagePer-project copies
Yarn PnP disk usageShared cache
Ghost dependency protectionnpm: no, Yarn PnP: yes
Ecosystem compatibilitynpm: universal, Yarn PnP: partial

Key takeaways:

  • Yarn was created to fix npm’s early problems. It introduced lockfiles, parallel installs, and deterministic dependency trees when npm had none of these .
  • Yarn’s PnP mode eliminates ghost dependencies. By enforcing that your code only imports what you declared, PnP prevents a class of production bugs that npm’s flat layout permits .
  • npm remains the universal default. It ships with Node.js, works with every tool, and requires zero setup. For simple projects, it is the path of least resistance .
  • Yarn’s speed advantage comes from avoiding disk writes. PnP installs are faster because they skip the file-copying step that dominates npm’s install time .
  • PnP compatibility is imperfect. Some tools assume node_modules exists. Yarn allows falling back to a traditional layout, but doing so sacrifices PnP’s strictness .
  • Yarn’s workspace tooling is more mature. Constraints, profiles, and the task runner make Yarn better suited for large monorepos .
  • Version pinning matters. Yarn’s packageManager field ensures every developer and CI runner uses the exact same Yarn version. npm relies on the Node.js bundled version .

Remember: Yarn and npm are not just different implementations of the same idea. Yarn’s PnP mode represents a philosophical commitment to strictness and correctness, while npm’s flat layout prioritizes compatibility and convention. The right choice depends on whether your project values the discipline PnP enforces or the universality npm guarantees.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!