Vite 8 Ships One Bundler, Not One Pipeline

Vite 7 depends on esbuild and Rollup; Vite 8 on neither. Serving one module from both majors shows what the swap to Rolldown changed and what it left alone.

Two steel mesh conveyor belts on a workbench converging into one wider belt that carries a machined housing away, with a small brass washer resting on the plate where they meet.

Two package manifests, one install of the same tiny project:

json
// [email protected] "dependencies"        // [email protected] "dependencies"
"esbuild": "^0.27.0 || ^0.28.0",    "rolldown": "~1.2.4",
"rollup":  "^4.43.0",               "lightningcss": "^1.33.0",

Two bundlers became one, and node_modules/esbuild is not in the Vite 8 tree at all. That much is accurate and easy to check. What it does not tell you is what a running dev server does differently, which is the thing an upgrade is actually scheduled on.

The response body barely moved

The launch article on this site walked one request through a Vite dev server and ended on the seam between the two engines. The cheapest way to see what became of that seam is to ask both majors for the same file. Same source tree, same [email protected], one dev server on each port:

bash
# Node 24.20.0, macOS arm64
curl -s http://localhost:5301/src/main.ts   # vite 7.3.6
curl -s http://localhost:5302/src/main.ts   # vite 8.2.2

The two bodies differ in exactly one token, an eight-character hash:

js
import { greet } from "/src/greet.ts";
import cloneDeep from "/node_modules/.vite/deps/lodash-es_cloneDeep.js?v=89b5fecc";  // v7
import cloneDeep from "/node_modules/.vite/deps/lodash-es_cloneDeep.js?v=1ee69a1c";  // v8

Relative specifier resolved to a served path, bare specifier rewritten to a pre-bundled dependency, nothing else touched. Both versions write the same files into node_modules/.vite/deps — one chunk per optimized entry plus a _metadata.json with the same fields, down to needsInterop on each entry. The request path is the same request path.

The enum is where the two compilers disagree

main.ts is too plain to show a transformer’s opinions. greet.ts contains an enum, and there the two responses stop matching:

js
// vite 7.3.6 — esbuild 0.28.2
var Level = /* @__PURE__ */ ((Level2) => {
  Level2[Level2["Low"] = 1] = "Low";
  Level2[Level2["High"] = 2] = "High";
  return Level2;
})(Level || {});
export function greet(p, level = 1 /* Low */) {
  const suffix = level === 2 /* High */ ? "!!" : ".";
js
// vite 8.2.2 — Oxc
var Level = /* @__PURE__ */ function(Level) {
	Level[Level["Low"] = 1] = "Low";
	Level[Level["High"] = 2] = "High";
	return Level;
}(Level || {});
export function greet(p, level = Level.Low) {
	const suffix = level === Level.High ? "!!" : ".";

Three differences, and two of them are cosmetic. esbuild renames the inner binding to Level2 where Oxc shadows with the same name; esbuild emits an arrow where Oxc emits a function expression. The emitted source maps agree that this is renaming and nothing more — Vite 7’s carries "names":["Level"], Vite 8’s carries "names":[].

The third difference is not cosmetic. esbuild constant-folds the member reads and Oxc does not. In the Vite 7 output the default parameter is the literal 1; in the Vite 8 output it is a property read on an object that has to exist by the time the function runs. Both are correct lowerings of the same enum, and they are not the same program.

A folded enum is a droppable enum

That difference does not stay in the dev server. Build both trees and grep the production chunk for a string that only the enum object contains:

bash
$ grep -c "High" dist/assets/*.js
0     # vite 7.3.6
1     # vite 8.2.2

Under Vite 7 the enum object is not in the shipped bundle. Nothing references it after the folding, so it is unreachable, and the bundler drops it. Under Vite 8 every read is a property access, the object is reachable, and it ships.

The cost is small and exactly measurable. Rewriting greet.ts to take a plain number instead of the enum, and rebuilding both:

                    with enum      without enum     difference
vite 7.3.6          12,908 B       12,908 B         0 B
vite 8.2.2          13,829 B       13,746 B        83 B

Zero against 83 bytes, for the same TypeScript. This is the whole tree-shaking argument in miniature: a bundler removes a module or a binding when it can prove nothing observable happens without it, and constant folding is what turns an enum read into that proof. Take the folding away and the proof goes with it.

Eighty-three bytes is not a reason to postpone an upgrade. A codebase with two hundred enums is a different arithmetic, and the arithmetic is now yours to do.

One toolchain writes both ends, and one always did

The mechanism is visible in Vite’s own bundle:

bash
$ grep -ho 'from "esbuild"' node_modules/vite/dist/node/*.js | sort -u        # vite 7
from "esbuild"
$ grep -ho 'from "rolldown[^"]*"' node_modules/vite/dist/node/*.js | sort -u  # vite 8
from "rolldown/experimental"

Vite 8 gets its transform from the bundler package, and that package is where Oxc lives: [email protected] declares @oxc-project/types pinned to 0.148.0 plus a platform binding. The word experimental is the package’s own path name, not a verdict here about stability.

It is worth being exact about what this fixed, because the common summary is that development and production used to run different engines. On the transform they did not. The folded level = 1 shows up in Vite 7’s production chunk as function Et(t,e=1), identical to what its dev server served, because esbuild handled that step at both ends already. What differed in Vite 7 was the bundler: esbuild pre-bundled dependencies and Rollup built for production. That is the pair Rolldown replaced.

So one engine now, in a real sense — and still two code paths. Development transforms file by file and serves unbundled modules. Production builds a graph and emits chunks. Nothing in Vite 8 merged those, and the enum result above is a reminder of why that matters: the dev server never runs the step that would have told you the object was droppable.

Cold start did not move, and the build moved both ways

Cold start, seven runs each, node_modules/.vite removed before every run:

$ rm -rf node_modules/.vite && npx vite --port <p> --strictPort   # x7, "ready in N ms"
vite 7.3.6   67 77 85 85 88 93 108     median 85 ms
vite 8.2.2   69 77 83 86 91 101 107    median 86 ms

One millisecond apart inside a spread of forty. Two modules and one dependency give a bundler nothing to be asymptotically better at, and the single first run I took before clearing the cache, 345 ms against 92 ms, measured dependency optimization rather than startup. vite build on the same tree does move, in both directions at once:

vite 7.3.6   112 modules   186 ms   12,908 bytes   gzip 4.67 kB
vite 8.2.2   113 modules    58 ms   13,829 bytes   gzip 4.92 kB

Roughly three times faster, and 921 bytes larger. Eighty-three of those bytes are the enum. The rest is the two minifiers having different habits: Rolldown quotes strings with backticks and declares with let where esbuild used double quotes and const. I have not traced which habit costs what.

What to run before you schedule the upgrade

Start both majors against the same tree and curl a module that uses your language features rather than a plain one. Enums, decorators, class fields and JSX are where two transformers get to have opinions, in the same way that reading what await compiles to tells you more than reading what it means. Then build both and compare sizes, because the number that got worse here is the one nobody advertises.

What I did not test: CSS, which moved to lightningcss and deserves its own measurement; any project large enough for bundler complexity to matter; and watch-mode rebuilds, where a persistent graph should pay off and where two modules have nothing to say.

FAQ

Frequently asked

Does the Vite 8 dev server bundle my application?

No. Measured on Vite 8.2.2, the dev server returns each module separately with its specifiers rewritten, exactly as Vite 7.3.6 does. Rolldown pre-bundles dependencies into node_modules/.vite/deps and builds for production; it does not bundle your source in development.

Why did my bundle get bigger after upgrading to Vite 8?

One measurable cause is TypeScript enums. esbuild folds Level.Low to the literal 1, which leaves the enum object unreferenced and lets the bundler drop it. Oxc emits the property read, so the object survives. On one scratch project that enum cost 0 bytes under Vite 7 and 83 bytes under Vite 8.

Arrow keys to move, Enter to open.