Every few months someone posts a benchmark showing Bun installing packages in 0.3 seconds and the entire dev Twitter loses its mind. Then six months later you're debugging a weird hoisting issue at 11pm and wondering why you didn't just stick with what worked. We've been through this cycle enough times that we have opinions now.
We use pnpm for everything we build at peal.dev. But that's not the answer to the question — the answer depends on what you're actually building and what kind of pain you're willing to tolerate. Let's go through all three honestly.
npm: The Default That's Actually Fine Now
npm has a reputation problem it doesn't fully deserve anymore. The npm from 2016 — the one with no lock file, non-deterministic installs, and a node_modules folder that took 30 seconds to install — is not the npm you get today. npm v7 added workspaces. npm v8 cleaned up the CLI. npm v9 and v10 are solid. If you're on a project by yourself and you don't care about disk space, npm is genuinely fine.
The problem isn't correctness, it's speed and disk usage. npm creates a flat node_modules structure where every dependency gets hoisted to the top level. This means your app can accidentally import packages it never declared as dependencies — called phantom dependencies — and everything just works until it doesn't. You upgrade something, the hoisting changes, and now you're importing a version of a package you never asked for.
# npm — standard stuff, works fine
npm install
npm install lodash
npm install -D typescript
npm run dev
# Workspaces (npm v7+)
# In package.json:
# "workspaces": ["packages/*"]
npm install --workspacesFor a solo project or a small team that doesn't do monorepos, npm is a totally reasonable choice. The lock file (package-lock.json) is verbose but reliable. CI/CD setups everywhere understand it. The ecosystem just works. If you're teaching someone Next.js for the first time, don't make them learn a new package manager on top of everything else.
pnpm: The One We Actually Use
pnpm solves the two things that actually annoyed us about npm: disk space and phantom dependencies. Instead of copying packages into every project's node_modules, pnpm stores packages in a global content-addressable store and hard-links them into your project. Install React 50 times across 50 projects? It only exists on disk once. This sounds like a minor optimization until you're on a laptop with 256GB of storage and you've been a developer for five years.
The bigger win for us is strict dependency isolation. pnpm creates a non-flat node_modules where packages can only access what they explicitly declared as dependencies. No more phantom dependencies silently saving your app. This catches real bugs early. We've had projects where switching to pnpm immediately surfaced a missing dependency that npm had been quietly ghost-importing for months.
# Install pnpm if you haven't
npm install -g pnpm
# Or use corepack (comes with Node 16+)
corepack enable
corepack prepare pnpm@latest --activate
# Daily usage — same as npm, mostly
pnpm install
pnpm add lodash
pnpm add -D typescript
pnpm dev
# The killer feature: filtering in monorepos
pnpm --filter @myapp/web dev
pnpm --filter "...@myapp/shared" build # build shared + everything that depends on itpnpm's monorepo support is where it really shines. The workspace protocol is clean, filtering is intuitive, and the performance difference versus npm in a monorepo with 10+ packages is significant. We're talking 2-3x faster installs in CI, which adds up when you're running pipelines 20 times a day.
The one gotcha: some packages are written assuming flat node_modules and break under pnpm's strict mode. You'll hit this occasionally with older packages or ones that have peer dependency issues. The fix is usually adding a .pnpmfile.cjs hook or adjusting shamefully-hoist-workspace-packages in .npmrc. Annoying but rare.
# .npmrc — fixes most compatibility issues
public-hoist-pattern[]=*
# Or more targeted — only hoist specific packages
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
# pnpm-workspace.yaml — for monorepos
# packages:
# - 'packages/*'
# - 'apps/*'Bun: Fast, Fun, and Occasionally On Fire
Bun is legitimately impressive. The benchmarks are real. Install times are absurd. Bun installs packages so fast it feels like it's doing something wrong. We clocked it at 4-5x faster than pnpm on a clean install of a mid-sized Next.js project. If your CI bill is mostly waiting for `npm install`, Bun will cut that down immediately.
But Bun is also doing three jobs at once — runtime, bundler, and package manager — and that ambition creates rough edges. The package manager part of Bun (which you can use independently of the runtime) is the most stable piece. You can drop it into an existing Node.js/npm project just for installs and it mostly works. The lock file format (bun.lockb) is binary, which means you can't easily diff it in pull requests, which is mildly annoying.
# Using Bun just as a package manager (doesn't require switching runtime)
bun install
bun add lodash
bun add -d typescript
bun run dev
# If you want human-readable lockfile
bun install --save-text-lockfile # creates bun.lock (text) in addition to bun.lockb
# Check what's installed
bun pm ls
# Clean install (like npm ci)
bun install --frozen-lockfileWe've had good results using Bun purely as a package manager in Next.js projects while keeping Node.js as the runtime. The speed benefit is real and the compatibility issues are minimal in this mode. Where we've run into trouble is using Bun as the full runtime in production. Some native Node.js modules don't work, some edge cases in the Bun runtime behave differently from Node, and when something breaks at 2am you're debugging a runtime that's still young and the Stack Overflow answers don't exist yet.
You can use Bun just for installs without committing to it as your runtime. This is the sweet spot right now — get the speed, skip the compatibility roulette.
The Real Comparison: What Actually Matters
Let's get past install speed benchmarks because honestly, for most projects, install speed is not your bottleneck. Here's what actually matters day-to-day:
- Lockfile diffs in PRs: npm and pnpm use text lockfiles you can actually read. bun.lockb is binary — you'll see 'Binary file changed' in your PR which is useless for review.
- Phantom dependency bugs: pnpm catches them by default. npm lets them slip through. Bun follows npm's flat model so same problem.
- Disk space: pnpm wins massively through its global store. Bun also has a global cache. npm duplicates everything.
- Monorepo ergonomics: pnpm is purpose-built for this. npm workspaces work but feel bolted on. Bun workspaces are improving but not there yet.
- Ecosystem compatibility: npm is 100%. pnpm is 95%+ with occasional hoisting fixes needed. Bun as a runtime is lower — probably 85-90% for real-world apps.
- Onboarding friction: npm wins. Everyone knows it. New team member, CI environment you didn't configure, Dockerfile someone else wrote — npm just works everywhere.
CI/CD: Where the Choice Actually Costs or Saves You Money
This is where package manager choice has a real dollar value. GitHub Actions charges by the minute. If your install step takes 90 seconds with npm and 30 seconds with Bun, and you run 100 pipelines a day, that's real money and real developer time.
# GitHub Actions — pnpm setup (fast and reliable)
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test# GitHub Actions — Bun setup (fastest installs)
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run build
- run: bun testBoth work well. pnpm with caching is fast enough that the difference from Bun is usually under 20 seconds for most projects — not nothing, but not life-changing. If you're already on pnpm and happy, don't migrate just for CI speed. If you're starting fresh and CI cost matters, Bun as a package manager with Node as runtime is worth considering.
Migration Paths: What Moving Actually Looks Like
We've migrated projects from npm to pnpm a few times. The process is usually: delete node_modules and package-lock.json, run pnpm import (which converts the npm lockfile to pnpm format), then pnpm install. Takes about 10 minutes. You'll probably find 1-2 packages that need the public-hoist-pattern fix. Worth doing for any project that's actively maintained.
Migrating to Bun as a package manager is similar — delete lockfile, run bun install. The binary lockfile thing is the main adjustment. Some teams add a step to generate a human-readable version for review purposes. Migrating to Bun as a runtime is a different, larger commitment that we wouldn't recommend for production apps without thorough testing of your specific dependencies.
Switching package managers is a Friday-afternoon task. Switching runtimes is a week-long project. Don't conflate the two.
What We Actually Recommend
Here's our honest take, not a hedge:
- Solo project or small team, no monorepo: npm is fine. Don't add complexity you don't need.
- Monorepo or multi-package setup: pnpm, no contest. The workspace support and strict dependency isolation are worth it.
- Performance-obsessed, greenfield project, willing to deal with occasional rough edges: Bun as package manager with Node runtime. Get the speed without the runtime risk.
- Startup with a real SLA and production traffic: pnpm. It's battle-tested, the ecosystem support is excellent, and you won't be debugging a runtime issue at 2am.
- Teaching or onboarding: npm. Seriously. Add complexity after they understand the basics.
All the templates on peal.dev ship with pnpm by default, which is a choice we made and stand behind — but the package manager isn't locked in. You can switch in five minutes. The important parts are the architecture patterns, auth setup, and payment integration, none of which care whether you're using npm, pnpm, or Bun.
The meta-point here is that package manager debates are a bit like arguing about which text editor is faster to type in. The fundamentals matter; the tooling choice at the margins matters less than how well you know whatever you picked. Pick one, learn its quirks, and move on to building the actual thing.
Our pick: pnpm for anything serious, npm for anything quick, Bun for package installation speed when you need it. Use Node as your runtime until Bun 2.x matures further.
