Vite, a dev server and a bundler in one
Vite is a build tool for browser applications that combines two separate things: a fast dev server built on native ES modules, and a production bundler. The team around VoidZero develops it under MIT, the current version is 8.2.1, and the repository holds roughly 82 thousand stars.
What Vite actually does
The name suggests one tool, while two different mechanisms run inside, and that is the first source of confusion.
The first mechanism is the dev server. When you run a project locally, Vite does not build a bundle out of the whole application. It serves source files to the browser as ES modules and transforms them one at a time, exactly at the moment the browser asks for them. A file nobody imported never gets processed. That is why server startup time barely depends on project size, which in a large application amounts to a difference measured in minutes.
The second mechanism is the production build, where the approach reverses. A classic bundle is produced, with chunk splitting, removal of unused code, and minification. The reason for that asymmetry is purely practical: hundreds of unbundled ES modules behave beautifully on a local disk and terribly over a network, where each one means a separate request and separate latency.
The third thing, easy to forget, is dependency pre-bundling. Libraries in node_modules change rarely and are often split across hundreds of files. Vite packs them once, on first run, and keeps the result in node_modules/.vite. The browser then fetches one file instead of three hundred, and modules written in the old CommonJS format get converted into the ES modules the browser requires anyway.
What Vite does not do matters just as much. It is not a framework and imposes no application structure, so it fits React, Astro or SvelteKit as readily as a project with no library at all. It does not run tests, although Vitest reuses the same configuration and the same plugin chain. It does not manage a monorepo, which is what tools like Turborepo are for. Nor does it ship a ready production server: server-side rendering is a set of programming interfaces rather than a closed solution.
Two modes, two different mechanisms
Separating the development mode from the production mode is the source of both the tool's greatest strength and its most irritating class of bugs.
The strength is obvious while working. Updating a module after saving a file touches only that module and its direct consumers. There is no rebuild of the whole graph, so response time does not grow with the project, and application state usually survives the swap.
The drawback shows up later. Since these are two distinct processing paths, they can differ in result. Code that works locally and breaks after a build is usually one of three things. First, a dependency that declares a different entry file for the browser than for Node, with export conditions resolving differently in each mode. Second, stylesheet ordering, which follows request order with unbundled modules and dependency graph order after a build. Third, code assuming files stay separate, such as paths assembled by hand from text fragments instead of through new URL with import.meta.url.
The practical conclusion is short: vite build and vite preview run locally, before a change reaches the repository, catch most such cases in under a minute. Teams that only do this on the build server pay for the saving with a fix cycle measured in hours.
Rolldown, and what version 8 changed
Through version 7, Vite rested on two separate engines: esbuild handled dependencies and transforms in development, while Rollup produced the production bundle. That split was a deliberate compromise dating back to the earliest releases, but it cost a divergence between what you see locally and what you get after building.
Version 8, released on 12 March 2026, ends the split. The bundler is now Rolldown, written in Rust, and transforms plus minification moved to tools in the Oxc family. The package dependencies show it plainly: vite 8.2.1 requires rolldown around version 1.2, rollup has left the dependencies entirely, and esbuild dropped to an optional peer dependency, declared under peerDependencies and marked optional. Stylesheet minification now runs through Lightning CSS by default.
The change has direct consequences in configuration. The field build.rollupOptions was replaced by build.rolldownOptions, optimizeDeps.esbuildOptions by optimizeDeps.rolldownOptions, and the esbuild section by an oxc section. The old names are still recognised and translated internally, but they are marked deprecated. Defaults shifted as well: build.target is now 'baseline-widely-available', and build.minify defaults to 'oxc' for client builds and false for server-side rendering builds.
The numbers quoted at release come from teams that tested the preview. Linear reports production build time falling from 46 seconds to 6 seconds, Ramp roughly 57 percent, Beehiiv roughly 64 percent, and Mercedes-Benz.io up to 38 percent. The scale depends on the project and shows most clearly where building was previously bounded by processor time rather than by disk operations.
There is also a cost worth knowing in advance. Vite 8 occupies about 15 megabytes more after installation than Vite 7, of which roughly 10 megabytes is Lightning CSS and 5 megabytes Rolldown itself. For container images built from scratch on every change, that is a visible line item.
Two things that descriptions from 2025 often conflate are worth separating. The rolldown-vite package was a distinct preview release, published while Rolldown was not yet the default, and its development stopped at version 7.3.1. There is no reason to reach for it today: in version 8 Rolldown is the default and only bundler, with no enabling flag.
Installation and requirements
Vite 8 requires Node 20.19 or newer within the 20 line, or 22.12 or newer. Older Node releases will not start at all, which on build servers with a pinned version tends to be the first problem during an upgrade.
# new project with an interactively chosen template
npm create vite@latest my-application
# template given directly, no questions
npm create vite@latest my-application -- --template react-ts
# adding to an existing project
npm install --save-dev vite
# run the dev server, build, and preview the result
npx vite
npx vite build
npx vite previewThe dev server listens on port 5173 by default. If the port is taken, Vite moves to the next free one, unless you set server.strictPort, which in that situation aborts startup rather than quietly changing the address.
Configuration and the names you need to know
Configuration lives in vite.config.ts at the project root. The defineConfig function does nothing at runtime and exists solely for type hints, so using it in a TypeScript project pays off from day one.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': '/src' },
dedupe: ['react', 'react-dom']
},
server: {
port: 5173,
strictPort: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
},
warmup: {
clientFiles: ['./src/main.tsx', './src/components/**/*.tsx']
}
},
optimizeDeps: {
include: ['lodash-es'],
exclude: ['@my-company/local-package']
},
build: {
target: 'baseline-widely-available',
sourcemap: true,
rolldownOptions: {
output: {
codeSplitting: true
}
}
}
})Three fields from that example dominate bug reports. resolve.dedupe resolves the case where two copies of the same library land in the bundle, which for libraries holding global state ends in a failure with no sensible message. optimizeDeps.exclude is needed for packages from the same monorepo, because pre-bundling freezes their state and changes to their source stop being visible. server.warmup transforms the listed files ahead of time instead of waiting for the first browser request.
Chunk splitting is configured through build.rolldownOptions.output.codeSplitting, and the detailed options are documented by Rolldown rather than by Vite. That matters when porting configuration from version 7, because the old manualChunks settings from Rollup do not carry over one to one.
Environment variables and values injected into code
Only variables carrying the VITE_ prefix reach browser code. That is a safeguard rather than an inconvenience: without it, one careless read would be enough to put a key from an .env file into the bundle shipped to users.
// files loaded in order: .env, .env.local, .env.[mode], .env.[mode].local
const apiUrl = import.meta.env.VITE_API_URL
// built-in fields, always available
console.log(import.meta.env.MODE) // 'development' or 'production'
console.log(import.meta.env.BASE_URL) // value of the base option
console.log(import.meta.env.PROD) // boolean value
console.log(import.meta.env.DEV)
console.log(import.meta.env.SSR)
// importing many files at once, expanded at build time
const pages = import.meta.glob('./pages/*.tsx')
const icons = import.meta.glob('./icons/*.svg', { eager: true })
// the correct way to build an asset path
const sound = new URL('./assets/click.mp3', import.meta.url).hrefThe import.meta.glob call is expanded at build time, so the pattern has to be written literally. Passing a variable there will not work, and this is one of the more frequent slips when porting code from webpack, where require.context behaved somewhat more loosely.
Plugins and their hook points
The Vite plugin system extends the Rollup plugin interface with several hooks of its own. A large share of existing plugins therefore works unchanged, while the Vite specific ones add dev server handling.
import type { Plugin } from 'vite'
export function timestampPlugin(): Plugin {
const virtualModule = 'virtual:build-time'
return {
name: 'timestamp',
enforce: 'pre', // 'pre' or 'post'
apply: 'build', // 'build' or 'serve'
config(userConfig, { command }) {
return { define: { __COMMAND__: JSON.stringify(command) } }
},
configResolved(resolved) {
console.log('mode:', resolved.mode)
},
resolveId(id) {
return id === virtualModule ? '\0' + virtualModule : null
},
load(id) {
if (id !== '\0' + virtualModule) return null
return `export const time = ${Date.now()}`
},
transformIndexHtml(html) {
return html.replace('</head>', '<meta name="build" content="ok"></head>')
},
configureServer(server) {
server.middlewares.use('/health', (_req, res) => res.end('ok'))
}
}
}The enforce field decides ordering relative to built-in plugins, and apply limits a plugin to one mode. That second option is underrated: a plugin analysing bundle size has nothing to do on the dev server, and its presence there can noticeably slow the first page load. The name field is not decoration, it appears in error messages and in profiling output.
Vite against the alternatives
| Feature | Vite | webpack | Rspack | Parcel | Turbopack |
|---|---|---|---|---|---|
| Engine | Rolldown and Oxc, Rust | JavaScript | Rust | Rust and SWC | Rust |
| Development mode | native ES modules | full bundling | full bundling | full bundling | incremental bundling |
| Configuration | small, one file | extensive | webpack compatible | none required | tied to Next.js |
| Plugin ecosystem | Rollup and Vite | largest | webpack plugins | its own | internal |
| Typical use | applications and libraries | older projects | migration from webpack | small projects | Next.js |
| Licence | MIT | MIT | MIT | MIT | MPL 2.0 |
The choice usually comes down to two questions. If you have a working webpack configuration and hundreds of lines of loader rules, Rspack lets you keep almost all of it, while Vite requires rewriting from scratch. If you are starting a project, or using a framework that already sits on Vite such as Nuxt, the question of alternatives loses practical meaning.
Common mistakes
The first is confusing the dev server with a production server. The vite preview command exists to check a built bundle on your own machine and lacks the protections required of a service exposed to the internet. Static files from the dist directory belong behind a proper HTTP server or a content delivery network.
The second is reading environment variables through process.env in browser code. That object does not exist in a browser and Vite does not substitute it automatically. The only correct route is import.meta.env with the VITE_ prefix, or the define field for values fixed at build time.
The third is a stale dependency pre-bundle. After a library version change, or a manual edit inside node_modules, the contents of node_modules/.vite stop matching reality, and the symptom is imports pointing at code that no longer exists. Running with the --force flag rebuilds that directory from scratch and settles the matter in seconds.
The fourth is forgetting the base option when deploying under a subdirectory. The default value assumes the application sits at the domain root. If it lands under an address with an extra path segment, every asset reference points into nothing, and the page loads as a bare HTML document without styles or scripts.
The fifth concerns stylesheets processed by external tooling. A Tailwind CSS setup, or any other preprocessor, is a separate layer that Vite merely invokes. Slow style rebuilds almost never come from Vite itself, but from file patterns defined too broadly in that layer's configuration.
The sixth is installing plugins purely to run a package manager other than the default. Vite runs on Node, but starts equally well through Bun or pnpm without extra plugins, and the differences amount to dependency install time rather than to how anything is built.
FAQ - frequently asked questions
Does version 8 require enabling Rolldown separately?
No. In Vite 8 Rolldown is the default and only bundler, with no flag involved. The rolldown-vite package, published during the preview phase, is no longer needed and stopped at version 7.3.1.
Does migrating from version 7 require rewriting the configuration?
In a typical project, no. The names build.rollupOptions and optimizeDeps.esbuildOptions are still recognised and translated internally into the new equivalents. What does require rewriting is detailed chunk splitting settings and plugins reaching directly into Rollup interfaces.
Why does the application work locally but come up blank after a build?
Usually because of a base option mismatched to the deployment address, or a code bug that surfaces only after minification. Run vite build and vite preview locally, then check the network tab in browser tools: requests ending in 404 point at base, while a console error points at the second cause.
Is Vite suitable for building libraries?
Yes, that is what library mode is for, configured through build.lib with the fields entry, name, fileName and formats. Default formats depend on the number of entry points: one entry yields es and umd, multiple entries yield es and cjs.
How do I speed up server startup in a large project?
Start with server.warmup for files that always load, then check whether some dependency is needlessly excluded from pre-bundling. The opposite move, adding heavy packages to optimizeDeps.include, helps when Vite discovers them mid-session and reloads the page.
Does Vite support server-side rendering?
It does, but as a set of programming interfaces on which a final solution is built. Ready-made handling comes from frameworks sitting on top of Vite, so for a production project it is wiser to reach for one of them than to assemble a server from scratch.
Documentation lives on the Vite site, the version 8 announcement on the project blog, and the source code in the GitHub repository.