The bundle-first problem
Before Vite, the standard model for a JavaScript dev server was simple and expensive: bundle the entire application from its entry point, hold the bundle in memory, and serve it to the browser. When the user edited a file, the bundler invalidated the parts of the bundle that depended on that file and rebuilt them. The server only became ready once the first full bundle existed.
This works. It also degrades in a predictable, linear way. A project with two hundred modules cold-starts quickly. A project with twenty thousand modules cold-starts in tens of seconds, and that cost is paid every time you restart the server - which, in real workflows, is many times a day. Bundlers added persistent caching, lazy chunking, and parallel workers to fight the slope, but the fundamental shape did not change: the bundler has to traverse the entire graph before the server can answer the first request.
Vite was built around an observation that became true sometime around 2020: every browser modern enough to ship to now natively understands ES modules. The browser itself can be the linker. Once that is on the table, the dev server's job is not to produce a bundle - it is to resolve, transform, and serve individual modules on demand. The shape of the problem changes from "process everything before the first request" to "process only what the browser actually asks for".
Two kinds of code, treated differently
The trick is that not all code in your project deserves the same treatment. Vite makes one clean cut down the middle:
- Dependencies. Anything under
node_modules. Plain JavaScript that almost never changes during development. Frequently shipped in CommonJS or as hundreds of tiny ESM files (thinklodash-es, which exports each function as its own module). Often slow to parse but trivial to cache. - Source code. Your application code. Frequently edited. Almost always written in a form the browser cannot run directly - TypeScript, JSX, Vue and Svelte single-file components, CSS modules, asset imports, and so on.
These two categories have opposite cost profiles. Pre-bundling dependencies is cheap to do once and cripplingly expensive to do on every request. Bundling your source code is expensive to do once and irrelevant most of the time, because most of it is not on screen. Treating them the same is what made bundle-first dev servers slow. Vite splits them.
Dependency pre-bundling
When the dev server starts (or on subsequent runs, when a cache miss is detected), Vite scans your source code for bare module specifiers - the import x from "some-package" form - and resolves each one against node_modules. For every dependency it finds, it runs a single fast pre-bundling pass.
The pre-bundler has two non-negotiable jobs:
- Convert CommonJS and UMD to ESM. The browser only understands ES modules. A large slice of npm still ships as CommonJS. Vite uses the bundler to convert these into ESM-compatible output so the browser can
importthem like anything else. - Collapse many small files into few.
lodash-esalone ships hundreds of separate ESM files, each one exporting a single function. If the browser had to issue an HTTP request for every one of them, page load would crawl, even over HTTP/2. Vite bundles each dependency into a small number of chunks - usually one - so the browser can fetchimport { debounce } from "lodash-es"in one round trip.
The output is written to node_modules/.vite/deps and served with strong HTTP cache headers (Cache-Control: max-age=31536000, immutable). The browser then caches each pre-bundled dependency for a year. On the next dev-server restart, the browser does not refetch anything that has not changed.
Pre-bundling re-runs when one of three things changes: the package lockfile, the resolved set of dependencies, or the Vite config that affects optimization. A cache metadata file under .vite/deps/_metadata.json holds the hash; on mismatch, the cache is invalidated and the bundler runs again. You can force a fresh pass with vite --force or by deleting the directory.
Source code as native ESM
Your own code is the half of the project that changes constantly. Vite does not bundle any of it during development. The dev server is an HTTP server that resolves a URL like /src/App.tsx and returns the transformed content of that one file. The browser - via native <script type="module"> and import statements - drives the rest.
The transform is a plugin pipeline. Each request runs through the same plugin chain Vite uses in production, in the same order. For a typical file, the pipeline might:
- Resolve bare specifiers to URLs that point at
/node_modules/.vite/deps/output (so the browser knows where to fetch them from). - Strip TypeScript types, transform JSX, run framework-specific compilers (Vue's SFC compiler, Svelte's preprocessor, etc).
- Process CSS imports (modules, PostCSS, preprocessors) into JavaScript modules the browser can
import. - Inline static assets below a configurable size threshold, or rewrite their import to a static URL.
Crucially, nothing is processed until the browser requests it. If the user only ever navigates to the home route, Vite never compiles the admin dashboard. Dynamic import() calls in your source are natural split points - Vite serves the lazy module the moment the browser actually asks for it, and not a millisecond before.
Hot Module Replacement
HMR in a bundle-based dev server has a structural problem: when one file changes, the bundler has to figure out which chunks are affected, rebuild those chunks, and push them down. The work scales with chunk size, not with the size of the edit. Edit a leaf component and you can still wait several hundred milliseconds for a fat chunk to rebuild.
Vite's HMR exploits the same property as its dev server: modules are individually addressable. When a file changes on disk, Vite knows exactly which module URL it corresponds to. It walks the module graph upward from that file, finding the first ancestor in each chain that has declared itself an HMR boundary via import.meta.hot.accept(). Only those modules need to be re-fetched by the browser, over a persistent WebSocket connection.
Framework plugins wire this up so application developers rarely write import.meta.hot by hand. The Vue plugin marks every SFC as a boundary; the React plugin uses React Refresh; Svelte's plugin handles it inside the compiler. The end result is the same: edit a leaf, and the browser receives a single new module and a single state-preserving replacement, with no full reload.
The reason this scales is the absence of bundle work. There is no chunk to recompute. The HMR cost is the cost of transforming exactly one file plus a WebSocket frame - and that cost is constant in the size of the project.
Why bundling still matters for production
If native ESM works so well in development, the obvious question is: why bundle at all in production? The short answer is HTTP overhead and tree shaking.
Browsers can load ESM natively, but a deeply nested module graph means a deeply nested chain of round-trip requests. Even on HTTP/2 with multiplexing, the discovery latency dominates - each module's imports are only known once the module itself has parsed. Shipping unbundled ESM to a real user on a real network is measurably slower than shipping a small number of chunks the browser can stream in parallel.
Bundling also unlocks the optimizations that matter at production scale:
- Tree shaking. Dead-code elimination across module boundaries, including library code, requires a static graph the bundler can prove things about.
- Chunk splitting. Manual and automatic strategies for grouping modules so route boundaries and dynamic imports become meaningful download units.
- Cross-module minification. Property mangling, scope hoisting, and constant folding work better with a single optimization pass over related modules.
- Asset fingerprinting. Content hashes in filenames let the build emit one set of files cached forever, replaced atomically by a new set on the next deploy.
Vite's build command does all of this. The question for most of Vite's history was: with what?
The unified toolchain: Rolldown and Oxc
Through Vite 7, the dev pipeline and the build pipeline used different bundlers. esbuild handled dependency pre-bundling and source transforms in development. Rollup handled the production bundle. The split was deliberate - esbuild is extraordinarily fast at the narrow set of jobs the dev server needs, Rollup is the most mature tree-shaking bundler in JavaScript - but it also meant two tools, two plugin systems, two configuration models. The plugin authors had to ship work for both. Subtle differences could appear between what worked in dev and what shipped in production.
Rolldown is the project that collapses both halves into one bundler. It is written in Rust, designed by the Vite team, and has two non-negotiable design constraints:
- Compatible with Rollup's plugin interface. The existing plugin ecosystem must keep working. This is what made the migration possible without breaking everything downstream.
- Fast enough to replace esbuild in the hot path. Vite's dev experience is the project's calling card. Rolldown has to match esbuild for the operations Vite actually performs.
Underneath Rolldown sits Oxc - the parser, transformer, and minifier the Vite team is building in parallel. Oxc is written in Rust, exposes a JavaScript API, and is designed to be a drop-in replacement for the patchwork of tools (Babel, Acorn, Terser, ESLint) that JavaScript build pipelines historically stitched together. Rolldown uses Oxc's parser and minifier; framework plugins can use Oxc directly when they need parsing or transformation without going through a bundler.
The end state is one toolchain, in one language, with one plugin API, used the same way in development and in production. The dev/build inconsistency that haunted earlier dev-server architectures - the gap where something would work locally and break on deploy - shrinks to whatever the configuration intentionally makes different. The unification is the point.
This is also why VoidZero exists. Evan You co-founded the company to keep Vite, Vitest, Rolldown, and Oxc moving as a coherent system rather than four open-source projects on parallel tracks. The unified toolchain is the deliverable; the company is the institution funding the engineering to make it real.