Tooling and buildThe build pipelinePart 2
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 package manifests, one install of the same tiny project:
// [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:
# 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.2The two bodies differ in exactly one token, an eight-character hash:
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"; // v8Relative 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:
// 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 */ ? "!!" : ".";// 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:
$ grep -c "High" dist/assets/*.js
0 # vite 7.3.6
1 # vite 8.2.2Under 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 BZero 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:
$ 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 msOne 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 kBRoughly 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.


