Yarn 01 🧶 Introduction to Yarn vs npm
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
| Feature | npm | Yarn |
|---|---|---|
| Ships with Node.js | Yes | No (via Corepack) |
| Default layout | Flat node_modules | PnP (no node_modules) |
| Ghost dependency protection | No | Yes (PnP mode) |
| Lockfile | package-lock.json | yarn.lock |
| Workspaces | Basic (since v7) | Advanced (constraints, profiles) |
| Dependency patching | No | Yes (yarn patch) |
| Task runner | No | Yes |
| Node version management | No | Yes (yarn switch) |
When to Choose npm
| Scenario | Reason |
|---|---|
| Maximum compatibility | Every tool supports npm |
| Zero configuration | Already installed with Node.js |
| Simple projects | Complexity not justified |
| Legacy codebases | Migration cost rarely worth it |
| Open-source contribution | Contributors expect npm |
When to Choose Yarn
| Scenario | Reason |
|---|---|
| Strict dependency enforcement | PnP eliminates ghost imports |
| Monorepo management | Constraints and profiles |
| Maximum install speed | PnP avoids disk writes |
| Zero-install workflows | Commit .yarn/cache to git |
| Team standardization | Pin 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Ghost imports break in production | npm’s flat layout allows undeclared imports | Declare all dependencies explicitly |
| PnP breaks a build tool | Tool assumes node_modules exists | Fall back to nodeLinker: node-modules |
| Yarn not installed on CI | CI uses npm by default | Enable Corepack or install Yarn explicitly |
| Lockfile conflicts in git | Multiple developers edit dependencies | Resolve manually; regenerate lockfile |
| Migration breaks peer dependencies | Yarn enforces strict peer deps | Add overrides or use npm |
| Disk space wasted | npm copies packages per project | Use 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
| Item | Value |
|---|---|
| npm default layout | Flat node_modules |
| Yarn default layout | Plug’n’Play (no node_modules) |
| npm lockfile | package-lock.json |
| Yarn lockfile | yarn.lock |
| npm workspaces | Basic, since v7 |
| Yarn workspaces | Advanced: constraints, profiles, catalogs |
| npm install speed | Baseline |
| Yarn PnP install speed | 2–4× faster |
| npm disk usage | Per-project copies |
| Yarn PnP disk usage | Shared cache |
| Ghost dependency protection | npm: no, Yarn PnP: yes |
| Ecosystem compatibility | npm: 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_modulesexists. 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
packageManagerfield 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.
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!