What this project runs
- Turbopack, the default in Next.js 16 (this app is on
next@16.1.6), for both next dev and next build. Nothing in the scripts overrides it — "dev": "next dev" and "build": "next build" both take the default. - The one exception is the analyzer:
"analyze": "ANALYZE=true next build --webpack" forces the webpack builder, because @next/bundle-analyzer doesn't support Turbopack. That --webpack flag exists purely for this reason. - So: Turbopack for real dev and prod builds, webpack only when you run
pnpm analyze.
Is it the best? Should we switch?
Short answer: yes, keep it — and the split setup above is best-practice, not a compromise. The longer answer starts with a constraint people forget.
- Inside Next.js there are only two builders: Turbopack and webpack. You can't drop in Vite, esbuild, or Rollup for a Next app — the framework is coupled to its bundler. So "switch bundlers" really means one of three different questions.
- Turbopack vs. webpack — Turbopack wins here. Stable and default in Next 16, dramatically faster dev HMR and builds, and it handles everything this app does (R3F / three.js, framer-motion, the App Router). The only friction is the analyzer, already solved with the separate
--webpack script. Forcing the whole project back to webpack to dodge one workaround trades everyday speed for a tool you run occasionally. Bad trade. - Rspack (the fast, webpack-compatible engine) — not worth it. Its selling point is webpack-plugin compatibility at speed, which solves a problem this app doesn't have.
- Leaving Next.js entirely (Vite + TanStack Start / React Router) — a multi-week migration that only pays off if you're fighting the framework. This app leans on SSR, the App Router, middleware, ISR (
revalidate), and dynamic rendering. Switching would cost weeks to lose features.
The one caveat worth holding: because Turbopack and webpack are different engines, the pnpm analyze treemap reflects the webpack build, not the Turbopack one that actually ships. Close enough to be directionally useful (what's in the bundle, roughly how big), but don't read it as byte-exact for production.
When a lead reaches for a different bundler
The senior framing: you rarely pick a bundler in the abstract. You pick it to fit what's being shipped and whichever constraint dominates. The situations that actually force it:
- You're building a library, not an app. The most common reason to step outside a framework default. npm packages need clean dual ESM+CJS output, externalized peer deps, generated
.d.ts, maximal tree-shakeability — so leads reach for Rollup (or tsup / unbuild, which wrap esbuild/Rollup), never a Next builder. Relevant here: this app consumes @paul-portfolio/css, /react, and /tokens. Whoever builds those packages is in library-bundler territory, not Turbopack. - A CLI or Node service. No browser, no HMR — you just want a fast single-file bundle. esbuild or
tsup; the browser-focused machinery of webpack/Vite is dead weight. - A huge existing webpack config you can't afford to rewrite. Hundreds of custom loaders/plugins, builds that take minutes. Leads reach for Rspack, deliberately webpack-API-compatible, for near-Turbopack speed as a mostly drop-in swap. The driver is migration cost, not raw capability.
- Micro-frontends / Module Federation. Independently-deployed frontends stitched at runtime — webpack or Rspack, because their Module Federation support is the mature option. The architecture dictates the bundler.
- The framework already decided. For most apps this is the real answer: Remix / SvelteKit / Astro / Nuxt hand you Vite; Next hands you Turbopack/webpack. The bundler choice collapses into the framework choice.
- Zero-config prototyping. Throwaway spike, no time for config — Parcel. Rare in production, handy for demos.
The mental model
- A bundler choice is downstream of two things: the deliverable (app vs. library vs. CLI vs. micro-frontend) and the dominant constraint (dev speed, output correctness, migration cost, ecosystem maturity, team familiarity). Name those two and the tool falls out almost mechanically.
- For an app inside a framework — this project — the framework picks for you and you inherit a good default. Overriding it is the exception, and it needs a reason bigger than a preference.
- The moment you're publishing packages — like the
@paul-portfolio/* design system this site depends on — is exactly when a lead consciously switches to Rollup/tsup, because now output shape is the constraint that matters most. Same person, same monorepo, different bundler, because the deliverable changed.
The takeaway
- "Should we use a different bundler?" is the wrong first question. "What are we shipping, and what's the constraint that hurts most?" is the right one — the bundler is the answer, not the question.
- For this app, the good decision was already made: Turbopack by default, webpack only for the analyzer. No change recommended.
- Keep a map in your head of tool → constraint (Rollup for library output, esbuild for CLI speed, Rspack for webpack-config migration, webpack for Module Federation, Vite via the meta-framework, Parcel for zero-config). Then the choice is never about taste.
What I'd do now, and what I did
- A written position on what this project runs and why, rather than switching on the basis of benchmarks that measure someone else's app.
- The conditions under which I would actually reach for something else, stated up front so the decision is not re-litigated every release.
Where it could go further
- The position is not re-measured. It was true when written and nothing prompts a recheck as the tooling moves.
- Build times are not tracked over time, so 'is this still fine' is answered by feel.
Next on this
- Nothing scheduled. This is a decision record, and the honest trigger to revisit it is a real problem rather than a date.