惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
Stack Overflow Blog
Stack Overflow Blog
D
DataBreaches.Net
罗磊的独立博客
博客园 - 司徒正美
Last Week in AI
Last Week in AI
The Cloudflare Blog
大猫的无限游戏
大猫的无限游戏
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
The GitHub Blog
The GitHub Blog
宝玉的分享
宝玉的分享
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
小众软件
小众软件
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
B
Blog
博客园 - 【当耐特】
V
V2EX
Apple Machine Learning Research
Apple Machine Learning Research
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
pnpm vs npm vs yarn in 2026: I ran all three on my real m...
Juan Torchia · 2026-05-10 · via DEV Community

pnpm vs npm vs yarn in 2026: I ran all three on my real monorepo and it forced me to change my mind

The correct answer for speeding up installs in a monorepo is to make hoisting stricter. I know that sounds backwards. More strictness should mean more compatibility errors, more debugging time, more friction. And yet that's exactly what pushed me to adopt pnpm — after it first broke a Radix UI dependency at the worst possible moment.

That's the honest trade-off no synthetic benchmark shows you: pnpm is faster and leaner, but its strict hoisting model has teeth. When it bites, it hurts. And the official migration guide doesn't warn you when it's about to bite.

A few months ago I was deep in a sprint — Next.js 16 monorepo, strict TypeScript, Shadcn/ui, Radix UI, everything running on Railway. I switched from npm to pnpm following the usual benchmarks — you know the ones, they measure a react install with three dependencies on a clean machine. In production, the result was different.

My thesis: pnpm wins the overall comparison in 2026, but the compatibility cost is real and measurable. Yarn Berry is the hardest to justify today. And npm improved so much in v10 that it's no longer the obvious thing to throw out.


pnpm vs npm 2026 in a monorepo: the numbers that actually matter

I ran all three on the same project — a monorepo with two Next.js 16 apps and a shared TypeScript utilities package. Same machine, same clean lockfile, same connection. CI numbers came from Railway with cache disabled to measure the real cold install.

Install time (cold cache, Railway CI)

Package Manager Install time Disk usage (node_modules)
npm 10.9 87s 1.4 GB
yarn berry 4.5 72s 890 MB (PnP mode)
pnpm 9.15 41s 610 MB

pnpm is ~53% faster than npm on a cold install and uses less than half the disk. Yarn Berry with PnP is interesting on disk, but the install time doesn't justify PnP's compatibility cost — which is even more aggressive than pnpm's.

With CI cache warm (the everyday scenario), the gap compresses but doesn't disappear:

# Warm cache — same project, three runs averaged
# npm: ~18s | yarn berry: ~14s | pnpm: ~9s

Enter fullscreen mode Exit fullscreen mode

The case that changed my mind: Radix UI and strict hoisting

This is what benchmarks don't measure. pnpm by default doesn't do flat hoisting like npm. Each package can only import what's declared in its own package.json. In theory, that's correct. In practice, there are dependencies that rely on npm's ghost hoisting — they access packages they never explicitly declared.

It happened to me with a specific version of @radix-ui/react-dialog that internally depended on @radix-ui/react-compose-refs without properly declaring it in its own package.json. npm resolved it silently through flat hoisting. pnpm blew up with a cryptic error:

# Error that showed up in the Next.js 16 build
# Cannot find module '@radix-ui/react-compose-refs'
# Require stack:
#   - node_modules/.pnpm/@radix-ui+react-dialog@1.0.5/node_modules/@radix-ui/react-dialog/dist/index.js

# This is not your bug — it's the dependency failing to declare its own dep

Enter fullscreen mode Exit fullscreen mode

The fix that worked while I waited for the upstream patch:

# .npmrc at the monorepo root
# Enable public hoisting for Radix packages that have this problem
public-hoist-pattern[]=@radix-ui/*
public-hoist-pattern[]=@floating-ui/*

Enter fullscreen mode Exit fullscreen mode

This .npmrc tweak tells pnpm to do public hoisting for those specific scopes, replicating npm's behavior only where it hurts. Not elegant. Pragmatic.


Real pnpm monorepo configuration

If you're going to run pnpm in a Next.js 16 monorepo, this is the configuration that survived production. Not the 10-minute tutorial config — the one that emerged after two weeks of debugging:

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  # exclude e2e folders so they don't step on monorepo deps
  - '!**/e2e/**'

Enter fullscreen mode Exit fullscreen mode

# .npmrc — monorepo root
# Public hoisting for packages that treat npm's flat hoisting as a feature
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=@radix-ui/*
public-hoist-pattern[]=@floating-ui/*

# Strict mode for everything else — pnpm's default
node-linker=node-modules

# Shamefully hoist: NEVER enable this in production
# shamefully-hoist=true  ← this is giving up; it turns pnpm into expensive npm

Enter fullscreen mode Exit fullscreen mode

The shamefully-hoist=true you see in some tutorials is total surrender. If you enable it, you're running pnpm with npm's behavior — you pay the cost of learning pnpm without taking any of the strictness benefits with you.

Workspace protocol and internal dependencies

// packages/ui/package.json  shared package
{
  "name": "@my-monorepo/ui",
  "version": "0.0.1",
  "dependencies": {
    // workspace:* tells pnpm to resolve from the local workspace
    // never from npm registry  this is critical for development
    "@my-monorepo/utils": "workspace:*"
  }
}

Enter fullscreen mode Exit fullscreen mode

// apps/web/package.json
{
  "dependencies": {
    "@my-monorepo/ui": "workspace:*",
    // exact Next.js 16 version  no ranges in production
    "next": "16.0.2"
  }
}

Enter fullscreen mode Exit fullscreen mode


The gotchas no synthetic benchmark measures

1. Lifecycle scripts and pnpm's PATH

pnpm doesn't add dependency binaries to your PATH the same way npm does. If you have scripts that call next or tsc directly in the shell (not via package.json scripts), they'll fail:

# This fails with pnpm if next isn't in your global PATH
$ next build

# This always works — pnpm resolves the workspace binary
$ pnpm next build
# or via package.json script:
# "build": "next build"

Enter fullscreen mode Exit fullscreen mode

2. pnpm dlx vs npx — they're not the same thing

# npx installs and caches globally by default
npx create-next-app@latest my-app

# pnpm dlx installs to a temp directory, no caching
# cleaner, slower on repeat runs
pnpm dlx create-next-app@latest my-app

# For tools you use frequently, install globally:
pnpm add -g @railway/cli

Enter fullscreen mode Exit fullscreen mode

3. Strict TypeScript and implicit re-exports

With strict TypeScript and pnpm, implicit re-exports from poorly-typed packages break earlier — which is actually an advantage disguised as a problem. pnpm forces you to discover implicit dependencies that npm would have never surfaced. That happened to me with a utilities library that was re-exporting lodash types without having it in its own dependencies.

This connects to something I already covered in the post about supply chain attacks in npm vs PyPI: the implicit dependency graph is exactly where the most interesting attack vectors live. pnpm makes that graph explicit. That's uncomfortable at first and valuable afterwards.

4. Railway CI and the pnpm store cache

Railway doesn't cache node_modules by default. With npm that stings a little. With pnpm it stings less because pnpm's store lives separately from the project:

# In your Dockerfile or Railway config
# Cache the pnpm store, not node_modules
ENV PNPM_HOME="/root/.local/share/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

# The store lives outside the project — cacheable across builds
RUN pnpm config set store-dir /root/.pnpm-store

Enter fullscreen mode Exit fullscreen mode

If you don't set this up, every Railway build does a cold install even when the lockfile didn't change. The separate pnpm store is the feature that impacts CI the most — more than the raw install time itself.

5. Yarn Berry in 2026: who is it even for?

Being honest: I couldn't find a use case in my stack where Yarn Berry was the right answer. PnP breaks more things than pnpm's strict hoisting, the documentation assumes you know exactly what you're doing, and the install time advantage over pnpm isn't enough to justify the friction.

Yarn Berry makes sense if you're coming from a massive monorepo already configured with PnP and you don't want to migrate. If you're starting from scratch today, pnpm is the more direct answer. This isn't tribalism — it's that I couldn't find a single benchmark of my own where Yarn Berry won on something I actually cared about.


Compatibility table with the real stack

Dependency npm 10 yarn berry 4 pnpm 9
Next.js 16 ✅ (with sdk)
Shadcn/ui ⚠️ (PnP quirks) ✅ (with public-hoist)
Radix UI ⚠️ ⚠️ (versions < 1.1.x)
TypeScript 5.7
ESLint 9 ⚠️ ✅ (with public-hoist)
Prisma 6 ⚠️ (postinstall)

⚠️ = works but requires extra configuration not documented in the official README


FAQ: pnpm vs npm 2026 monorepo

Is it worth migrating from npm to pnpm on an existing project?

If the project is already in production and stable, evaluate it by CI cost. If your Railway pipeline (or any CI) runs installs frequently, the ~50% cold install difference adds up to real build hours per month. If the pipeline is short or already has aggressive caching, the urgency drops. The migration itself takes half a day plus two days of debugging edge cases — like the Radix UI one I described above.

What is pnpm's strict hoisting and why does it matter?

In npm, all packages are installed into a flat node_modules. Any package can access any other package, even if it didn't declare it as a dependency. pnpm instead creates a node_modules with symlinks where each package only sees what it declared. This prevents phantom dependencies but breaks packages that rely on npm's flat behavior. The official pnpm documentation explains the model in detail.

Does shamefully-hoist=true solve the compatibility problems?

Technically yes, but it's a partial surrender. If you enable shamefully-hoist=true, pnpm behaves like npm in terms of hoisting — you lose exactly the strictness benefit that makes pnpm valuable. The right alternative is public-hoist-pattern for the specific scopes that have the problem, not enabling global hoisting.

Is Yarn Berry with PnP better than pnpm in large monorepos?

Not in my benchmarks. Yarn Berry PnP has an interesting disk advantage but the compatibility cost is higher than pnpm's. On top of that, TypeScript tooling and IDEs have more stable support for pnpm's model than for PnP. For new monorepos in 2026, pnpm is the more pragmatic bet.

Did npm 10 improve enough that switching isn't worth it?

npm 10 improved quite a bit — workspaces work well, installs are faster than npm 8. But on disk and cold CI, the gap with pnpm is still substantial (610 MB vs 1.4 GB in my case). If you've got everything configured with npm and don't have a concrete disk or build time problem, the migration might not be worth the cost. If you're starting a new project, start it with pnpm.

How do I handle dependency updates in pnpm with a monorepo?

pnpm update --recursive --latest updates all apps and packages in the workspace at once. What I learned to do is run this on a separate branch, run the full build, and review lockfile changes before merging. With strict TypeScript, broken type changes show up in the build — which is exactly the safety net I described in the post about functional programming in TypeScript.


My final take (and what I don't buy about the viral benchmarks)

pnpm wins in 2026. That's not up for debate after seeing the numbers in real production. But the "just migrate and you're done" narrative that circulates in viral HN posts feels dishonest to me — or written by someone who's never run pnpm against Shadcn/ui with an outdated version of Radix UI.

The real cost of adopting pnpm isn't install time or learning the CLI. It's the day something breaks in production because a transitive dependency was relying on flat hoisting and nobody documented it. That day exists. It happened to me. It's fixable — but you need to know it's coming.

What I don't buy: that Yarn Berry is relevant for new projects in 2026 without a very specific use case. And I don't buy that shamefully-hoist=true is a valid solution — it's postponing the problem until someone on the team can't figure out why the monorepo behaves differently in local vs CI.

If you're coming from my same stack (Next.js 16, strict TypeScript, Shadcn/ui, Railway), the migration is worth it. Just do it with your eyes open: configure public-hoist-pattern for the UI scopes, cache the pnpm store in CI, and keep TypeScript strict as your safety net for when hoisting surfaces implicit dependencies. That's exactly what I'd do differently if I were starting from scratch.

In the meantime, if you're seeing weird "module not found" errors after migrating to pnpm, before you panic — check whether the package has its own dependencies properly declared. High probability the problem is upstream, not you.


Primary source:


This article was originally published on juanchi.dev