A client's marketing site went from a 4m10s production build to 38 seconds. We shipped it on a Tuesday and everyone was pleased. On the following Monday their support lead asked why the site had been flashing unstyled text on first load for a week, and why every JavaScript error in Sentry now pointed at line 1 of a file called index-B4kQ.js.
Nothing about that week had to do with the bundler being fast. The bundler was fast. It also emitted CSS in a different order, split chunks along different boundaries, and produced sourcemaps our release upload step no longer matched. Three regressions, all invisible in dev, all green in CI, all shipped.
We've done four of these migrations this year — two onto Vite 8 with Rolldown, one onto Rspack, one that came free with a Next.js upgrade. The performance numbers were real every time and decided nothing. What decided the work was whose plugin API we inherited and how far the output moved.
What actually ended was the two-bundler tax
For most of Vite's life you were running two different bundlers without thinking about it. esbuild transformed your modules in dev, Rollup produced the production build, and the two disagreed in small ways: interop with CommonJS dependencies, decorator and class-field ordering, how a circular import resolved, whether a side-effectful module got dropped. Vite 8 went stable in March 2026 and replaced that split with a single Rust pipeline in Rolldown — the project calls it its biggest architectural change since Vite 2.
Every team we work with has lost at least a day to that split. The shape is always the same: a bug that only exists in the production build, no stack trace worth reading, and a debug loop that costs a full build per attempt. You end up bisecting a 4-minute build with console.log, which is not engineering, it is waiting.
“The build was never slow enough to be a priority. It was inconsistent enough to be a tax, and nobody was writing that down.”
That is the honest headline for 2026. Vite reports up to 10–30× faster builds, and we measured 6.6× on the largest app we moved — but the number that changed how the team works was the one nobody publishes: dev and prod now fail the same way. A bug you can reproduce in a hot-reload loop is a bug you fix in ten minutes instead of two hours.
Four bundlers, and only two of them are a choice
These get compared as if they were four options on a shortlist. They aren't. Two of them are decided by a framework you already picked, one is decided by a config file you can't delete, and only one is a genuine adoption decision most weeks. Sorting them that way makes the comparison much shorter.
Rolldown — the one you get by upgrading Vite
Rolldown is a Rust bundler that speaks Rollup's plugin interface, and in Vite 8 it is the whole pipeline: dev, build, and the dependency pre-bundling esbuild used to handle. For an app on Vite this is an upgrade, not an adoption — you bump a version and most projects build on the first try.
The compatibility claim holds better than we expected, with one clear boundary. Plugins that use the documented hooks — resolveId, load, transform — moved without edits across all three codebases we tried. Plugins that reach into Rollup's internals, walk the module graph during generateBundle, or mutate chunk metadata are where the work is. In our case that was one in-house plugin doing route manifest generation, and it took an afternoon.
Turbopack — you don't pick it, Next.js picks it for you
Turbopack is not on the shortlist because there is no shortlist. If you're on a current Next.js, it builds your app, and the interesting question isn't whether it's faster than Rspack — it's how much control you gave up. The webpack escape hatch that used to absorb odd requirements is not the road ahead, and a good deal of the bundler's behaviour is framework policy rather than your configuration.
We think that trade is usually correct and we take it deliberately. The framework owning the bundler is why route-level code splitting, server/client boundaries and the caching model work without a config file. It is also why, when a build does something surprising, your debugging options are a GitHub issue rather than a plugin. Price that in before you put an unusual asset pipeline inside a Next app.
Rspack — for the webpack config nobody is allowed to rewrite
Rspack exists for a specific, extremely common situation: a webpack config that has grown for six years, that three people understand, and that contains at least one loader doing something load-bearing and undocumented. Rspack keeps the webpack API and swaps the engine underneath, so the migration is a config compatibility exercise rather than a rewrite.
It is the least glamorous option here and the one we have recommended most often on takeover projects. Compatibility is close but not total — the loaders that broke for us were the ones calling into webpack internals or doing their own AST work, the same category that breaks on Rolldown. If your config is mostly standard loaders and a handful of well-known plugins, budget days. If it has custom loaders written by someone who left, budget weeks and read them first.
esbuild — still everywhere, just not your app bundler
esbuild didn't lose. It moved. It is still the fastest way to turn TypeScript into JavaScript, and it is still sitting inside your test runner, your CLI build, your Lambda packaging step and half the tools you use daily. What it stopped being is the thing that assembles an application, and that was always by design: its plugin API is deliberately narrow, with no full AST access, because that narrowness is where the speed comes from.
Reach for it directly when you're shipping a library, a CLI or a serverless function — anything where the job is transform-and-emit and the output has one consumer. Don't reach for it when you need chunking strategy, CSS handling and asset hashing to cooperate. That's not a limitation anyone is hiding; it's the trade the tool announced on day one.
| Rolldown (Vite 8) | Turbopack | Rspack | esbuild | |
|---|---|---|---|---|
| Written in | Rust | Rust | Rust | Go |
| How you adopt it | Upgrade Vite | Use Next.js | Point it at your webpack config | It's already in your toolchain |
| Plugin surface you inherit | Rollup hooks | Next's, not yours | webpack loaders and plugins | Narrow by design, no full AST |
| Same engine in dev and prod | Yes — the actual win | Yes, within Next | Yes | N/A — it's a transform step |
| Where the migration hurts | Plugins using Rollup internals | Nothing to migrate, little to steer | Custom loaders touching internals | Anything needing real plugin power |
| Output shape moves | Chunk boundaries and CSS order | Framework's call, not yours | Close to webpack, not identical | N/A |
| Who owns a regression | You and the Vite team | The framework | You and the Rspack team | You |
| Choose it when | You're on Vite | You're on Next | The config is older than the team | You're building a library or CLI |

The three things that actually break
A bundler swap produces a class of regression your test suite is structurally unable to catch. Unit tests run against source. Component tests run against a dev transform. End-to-end tests run against a browser that will happily render a correct page from a badly-shaped bundle. Everything passes, and the damage lands in your CDN metrics and your error tracker instead.
- Plugin hooks that used private surface— the documented hooks port cleanly across all of these. The ones that walked the module graph or edited chunk metadata are where the days go, and they’re usually in-house code nobody has opened since it was written.
- Chunk boundaries and CSS order— a new splitting strategy means new filenames on files that didn’t change, which means a cold CDN and a cold browser cache for every returning visitor on release day. Different CSS emission order means specificity ties resolve the other way.
- Sourcemap fidelity and upload— maps that still exist, still validate, and no longer line up with what your release pipeline uploaded. Nothing errors. Your stack traces just quietly become useless.
The sourcemap one cost us the most and looked like the least. Sentry kept accepting events, the release still had artefacts attached, and every frame resolved to the top of a hashed file. For six days the team read production errors as if they were minified, assumed the errors were new, and chased two of them into the wrong subsystem. Nothing in the system was in a failed state — it was in a wrong state, which is worse, because failed states page someone.
The CSS one is the one to check first, because it is the one users see. Under Rollup our vendor styles landed before our utility layer; under the new pipeline two chunks swapped order and a handful of equal-specificity rules started resolving the other way. On a design system with tight token discipline this is a non-event. On a codebase carrying five years of overrides, it is a visual regression on pages nobody screenshots.
The pre-flight we run now
After the second migration we stopped treating this as a version bump and started treating it as an output-shape change, which is what it is. The checks below take about half a day to set up once and have caught something on every migration since.
Before you merge a bundler swap
- Build both ways and diff the manifest: chunk count, entry-to-chunk mapping, and the emission order of every CSS file.
- Take the ten most-visited routes and diff rendered HTML and computed styles between old and new builds, not screenshots of them.
- Throw a deliberate error in staging and confirm the stack trace in your error tracker still resolves to a real file and line.
- Check total transferred bytes per route, not total bundle size — a smaller bundle split worse is a slower page.
- Grep your plugins and loaders for anything importing from a bundler's internals; read those before you start, not when they fail.
- Plan the release for a low-traffic window and expect a cold-cache spike — every hashed filename that changed is a fresh download.
It is a hundred lines and it is not clever. What it buys is that output shape becomes something you approve rather than something you discover — and it keeps earning after the migration, because dependency upgrades move chunk boundaries too. Two of the three times it has fired since, the cause was a new package, not a bundler.

The short version
If you're on Vite, upgrade to 8 and take Rolldown — the single-engine consistency is worth the afternoon it may cost you, independent of the speed. If you're on Next, you already have Turbopack and the useful work is auditing anything unusual in your asset pipeline before it becomes someone else's policy. If you're carrying a webpack config older than half the team, Rspack is the pragmatic answer and the alternative is a rewrite you won't get funded. And keep esbuild exactly where it is, doing the job it's best at.
What we'd push back on is the framing these tools get sold with. Build speed is a developer-experience improvement and a CI bill, and both are worth having. It is not a user-facing performance win, and treating it as one is how a team ends up shipping a 6× faster build that made the site slower to load because the chunks split worse.
Run the swap on a branch, build it twice, and diff the two dist directories before you look at a single timing number. If the shape moved, you now know the thing your tests were never going to tell you. If it didn't, take the speed — you've earned it cheaply.









