Vite: The Lightning-Fast Build Tool
If you've ever stared at a terminal waiting 30 seconds for your development server to start, or watched a full-page reload wipe out your application state during development, you've felt the pain that Vite was built to solve. Vite (French for "quick") is a next-generation frontend build tool created by Evan You — the same developer behind Vue.js — and it has fundamentally changed how developers think about build tooling. In this guide, we'll explore why Vite is so fast, how to use it, and how to configure it for real-world projects.
Vite starts your dev server in under 300ms regardless of project size, because it skips bundling entirely during development. Dependencies are pre-bundled once with esbuild (written in Go, 10–100× faster than JS-based bundlers), while your own source files are served as native ES modules directly to the browser.
Why Traditional Bundlers Are Slow
To understand why Vite is fast, you need to understand why Webpack and similar tools are slow in development. Traditional bundlers must process your entire application before serving even a single page. For a large app with thousands of modules, this means:
- Cold start penalty: Every time you start the dev server, the bundler crawls your entire dependency graph, transforms every file, and bundles everything into a few output chunks before the browser gets anything.
- Slow HMR: When you change a file, the bundler must re-process all affected modules in that bundle chunk — which grows linearly with your app size.
- Memory pressure: Holding the entire dependency graph in memory causes Webpack to consume gigabytes of RAM on large projects.
The root cause is architectural: bundlers were designed when browsers couldn't understand ES modules natively. That's no longer true. Every modern browser has supported native ESM since 2018.
Webpack bundles everything upfront; Vite serves files as native ES modules on demand
Getting Started with Vite
Vite ships with an official scaffolding CLI that sets up a project with sensible defaults in seconds. It supports React, Vue, Svelte, Lit, Preact, vanilla JS, and TypeScript out of the box.
Scaffold a new project
Run the create command and follow the interactive prompts to choose your framework and variant.
# npm 7+
npm create vite@latest my-app
# yarn
yarn create vite my-app
# pnpm
pnpm create vite my-app
# Skip prompts: pass framework and variant directly
npm create vite@latest my-react-app -- --template react-ts
npm create vite@latest my-vue-app -- --template vue
npm create vite@latest my-svelte-app -- --template svelte-ts
Install dependencies and start the dev server
Navigate into the project, install, and start. The dev server will be ready almost instantly.
cd my-app
npm install
npm run dev
# Output:
# VITE v5.x.x ready in 287 ms
#
# ➜ Local: http://localhost:5173/
# ➜ Network: use --host to expose
Explore the project structure
Vite's scaffolded project is minimal and opinionated — no boilerplate noise.
my-app/
├── public/ # Static assets copied verbatim to dist/
│ └── vite.svg
├── src/
│ ├── assets/ # Assets processed by Vite (hashed, optimized)
│ ├── App.tsx
│ ├── main.tsx # Entry point — imported as native ESM
│ └── vite-env.d.ts
├── index.html # Root HTML — Vite entry, not public/
├── vite.config.ts
├── tsconfig.json
└── package.json
Unlike Webpack where you specify an entry JS file in the config, Vite treats index.html as the entry point. It parses the HTML for <script type="module"> tags and follows the import graph from there — mirroring how browsers actually load ESM applications.
How the Vite Dev Server Works
Understanding Vite's dev server architecture explains why it stays fast no matter how large your project grows. There are two categories of modules Vite treats differently:
Pre-bundled Dependencies (node_modules)
Packages from node_modules are pre-bundled once using esbuild when you first start the server (or when they change). esbuild is written in Go and compiles 10–100× faster than JavaScript-based tools. These pre-bundled packages are cached in node_modules/.vite/deps/ and served as single ESM files — solving the "10,000 small files" problem that would otherwise overwhelm the browser.
Source Files (your code)
Your own source files are never pre-bundled. When the browser requests a file, Vite transforms it on the fly (TypeScript, JSX, CSS modules, etc.) and serves it as an ES module. Only the files the browser actually requests get processed — so a large unused module contributes zero startup cost.
Hot Module Replacement (HMR)
Vite's HMR is precise: when you change a file, only that module is invalidated. The HMR update payload is tiny — just the changed module — regardless of how many other files exist in the project. This is why Vite's HMR feels instant even in very large apps where Webpack's HMR takes several seconds.
// Frameworks handle HMR automatically.
// Use the low-level API only for custom module state cleanup:
if (import.meta.hot) {
// Preserve state when module updates
import.meta.hot.accept('./store.ts', (newModule) => {
// newModule is the updated module
replaceStore(newModule.store)
})
// Clean up side effects (intervals, subscriptions, etc.)
import.meta.hot.dispose((data) => {
clearInterval(pollingId)
data.pollingId = pollingId // carry state to next version
})
}
Configuring Vite
Vite is configured via vite.config.ts (or vite.config.js). The defineConfig helper provides TypeScript intellisense for all options.
A typical vite.config.ts for a React project:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
// Base public path when served in production
base: '/',
// Environment variables prefix (default: VITE_)
envPrefix: 'VITE_',
})
Configure module resolution aliases to avoid long relative imports:
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
// "@/components/Button" instead of "../../components/Button"
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils'),
'@hooks': path.resolve(__dirname, './src/hooks'),
},
// Try these extensions in order when no extension is given
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
},
})
Configure the development server — port, proxy, CORS, HTTPS:
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 3000, // Default is 5173
open: true, // Open browser on start
host: '0.0.0.0', // Expose to network (for Docker, etc.)
strictPort: true, // Error instead of trying next port
// Proxy API requests to your backend during development
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
'/graphql': 'http://localhost:4000',
},
// CORS headers
cors: true,
// HTTPS (generate a local cert first)
// https: true,
},
})
Control production build output, code splitting, and minification:
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: 'dist', // Output directory
target: 'es2020', // Browser compatibility target
minify: 'esbuild', // 'esbuild' (fast) or 'terser' (smaller)
sourcemap: true, // Generate source maps
rollupOptions: {
output: {
// Split vendor code into a separate chunk
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
},
// Asset file naming
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
},
},
// Warn when a chunk exceeds this size (in kB)
chunkSizeWarningLimit: 500,
},
})
The Plugin Ecosystem
Vite's plugin API extends Rollup's interface, which means most Rollup plugins work with Vite out of the box. The ecosystem has grown quickly — here are the plugins every serious Vite project should know:
Official Framework Plugins
# React (JSX transform + Fast Refresh)
npm install -D @vitejs/plugin-react
# React with SWC (Rust-based transpiler, even faster)
npm install -D @vitejs/plugin-react-swc
# Vue
npm install -D @vitejs/plugin-vue
# Svelte
npm install -D @sveltejs/vite-plugin-svelte
Essential Community Plugins
| Plugin | What It Does | Install |
|---|---|---|
vite-plugin-pwa |
Zero-config PWA with service worker generation | npm i -D vite-plugin-pwa |
unplugin-vue-components |
Auto-import Vue components on demand | npm i -D unplugin-vue-components |
vite-plugin-compression |
Gzip/Brotli compress build output | npm i -D vite-plugin-compression |
rollup-plugin-visualizer |
Bundle size analysis with treemap visualization | npm i -D rollup-plugin-visualizer |
vite-plugin-svgr |
Import SVGs as React components | npm i -D vite-plugin-svgr |
vite-plugin-mock |
Mock API endpoints during development | npm i -D vite-plugin-mock |
Writing a Custom Vite Plugin
Vite plugins are just objects with hook functions. Here's a simple plugin that injects a build timestamp into your app:
import type { Plugin } from 'vite'
export function buildInfoPlugin(): Plugin {
return {
name: 'vite-build-info',
// Runs once at the start of the build
buildStart() {
console.log(`Build started at ${new Date().toISOString()}`)
},
// Transform a specific file's source code
transform(code, id) {
if (id.endsWith('src/main.tsx')) {
// Inject build time as a global constant
return code.replace(
'__BUILD_TIME__',
JSON.stringify(new Date().toISOString())
)
}
},
// Runs after the build completes
closeBundle() {
console.log('Build complete!')
},
}
}
// Usage in vite.config.ts:
// import { buildInfoPlugin } from './plugins/vite-build-info'
// plugins: [react(), buildInfoPlugin()]
Building for Production
Vite's production build uses Rollup under the hood — not esbuild. This is an intentional architectural decision: esbuild is blazingly fast but doesn't yet support all of Rollup's advanced code-splitting and tree-shaking capabilities. The result is highly optimized, tree-shaken bundles with automatic CSS code splitting.
Run the build command
Vite compiles and bundles your entire application into the dist/ directory.
npm run build
# Output:
# vite v5.x.x building for production...
# ✓ 42 modules transformed.
# dist/index.html 0.46 kB │ gzip: 0.30 kB
# dist/assets/index-Bx7QLdoA.css 1.39 kB │ gzip: 0.72 kB
# dist/assets/vendor-DcY7Z0lL.js 141.74 kB │ gzip: 45.64 kB
# dist/assets/index-BjnUr2H5.js 3.21 kB │ gzip: 1.49 kB
# ✓ built in 1.38s
# Preview the production build locally
npm run preview
# ➜ Local: http://localhost:4173/
Analyze your bundle
Use rollup-plugin-visualizer to understand what's in your bundle before shipping.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
react(),
// Only run during build: generates stats.html
visualizer({
open: true, // Auto-open in browser after build
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html',
}),
],
})
Deploy the dist/ folder
Vite builds are static files — deploy to Vercel, Netlify, Cloudflare Pages, S3, or any static host. Configure your server to redirect all 404s to index.html for client-side routing to work.
If you use React Router or Vue Router in history mode, configure your web server to serve index.html for all routes. On Nginx: try_files $uri $uri/ /index.html;. On Apache: use a .htaccess with RewriteRule ^ index.html. Without this, direct URL navigation causes 404 errors.
Environment Variables
Vite exposes environment variables to your client code through import.meta.env. Only variables prefixed with VITE_ are exposed to the browser — server-only secrets without the prefix stay private.
# .env (committed — shared defaults)
VITE_APP_TITLE=My App
VITE_API_BASE_URL=https://api.example.com
# .env.local (gitignored — local overrides)
VITE_API_BASE_URL=http://localhost:8000
# .env.production (used when building for production)
VITE_APP_TITLE=My App (Production)
# Server-only — NOT exposed to browser
DATABASE_URL=postgres://localhost/mydb
SECRET_KEY=super-secret-value
// Built-in Vite env vars
console.log(import.meta.env.MODE) // "development" or "production"
console.log(import.meta.env.BASE_URL) // "/" by default
console.log(import.meta.env.DEV) // true in development
console.log(import.meta.env.PROD) // true in production
// Your custom variables
const apiBase = import.meta.env.VITE_API_BASE_URL
const appTitle = import.meta.env.VITE_APP_TITLE
// TypeScript: add type declarations to vite-env.d.ts
// to get autocomplete for your custom vars:
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Vite uses Rollup for bundling and esbuild for minification in production
Vite vs Webpack: When to Use Which
Vite is the right choice for most greenfield projects. But Webpack still has a role — particularly in large enterprise codebases with complex, custom build pipelines built up over years. Here's an honest comparison:
| Feature | Vite 5 | Webpack 5 |
|---|---|---|
| Dev server cold start | <300ms (no bundling) | 5–60s (full bundle) |
| HMR speed | Instant (single module) | Slow on large apps |
| Production bundler | Rollup (excellent tree-shaking) | Webpack (highly configurable) |
| Configuration complexity | Minimal, sensible defaults | Complex, verbose |
| Plugin ecosystem | Growing, Rollup-compatible | Mature, vast |
| CSS/asset handling | Built-in (CSS Modules, PostCSS) | Requires loaders config |
| Module Federation | Via vite-plugin-federation | Native support (v5) |
| TypeScript | Zero-config (esbuild transpile) | Requires ts-loader/babel |
| Legacy browser support | Via @vitejs/plugin-legacy | Native (target option) |
| Best for | New projects, SPAs, SSR | Complex legacy migrations, MF |
Migrating an Existing Webpack Project to Vite
- Install Vite:
npm install -D vite @vitejs/plugin-react(or your framework plugin) - Move index.html: Move from
public/index.htmlto the project root and replace%PUBLIC_URL%placeholders with plain paths - Add script module tag: In
index.html, add<script type="module" src="/src/main.tsx"></script> - Update env vars: Replace
process.env.REACT_APP_*withimport.meta.env.VITE_* - Create vite.config.ts: Add your framework plugin and configure any aliases or proxies you had in Webpack
- Update package.json scripts: Replace
react-scripts start/buildwithvite/vite build - Handle CommonJS modules: Vite expects ESM. If dependencies use CJS, they'll be auto-converted by esbuild's pre-bundler — but some edge cases need manual handling via
optimizeDeps - Remove Webpack config and loaders: Webpack-specific files (
webpack.config.js, babel configs, etc.) can be deleted once the migration is confirmed working
Key Takeaways
Vite has earned its position as the default build tool recommendation for most new JavaScript projects — and for good reason. Here's what makes it worth adopting:
Why Vite Wins the Developer Experience Battle
- Instant dev server: No bundling means cold start in milliseconds, regardless of project size
- Precise HMR: Only the changed module is invalidated — your state survives edits
- Zero-config TypeScript: TS, JSX, CSS Modules, JSON, and Web Workers work out of the box
- Rollup production builds: Excellent tree-shaking, automatic code splitting, and asset fingerprinting
- Framework-agnostic: One tool for React, Vue, Svelte, Lit, and vanilla JS projects
- Small config surface: Sensible defaults mean most projects need fewer than 20 lines of config
The limitations worth knowing: Vite's dev/prod split (esbuild dev, Rollup prod) can occasionally reveal behaviour differences between environments. For micro-frontend architectures with Module Federation, Webpack 5 still has a more mature solution. And if you're migrating a large CRA or Webpack project, expect some friction with CommonJS-only dependencies and process.env references.
"Vite represents the natural evolution of frontend tooling — it stops fighting the browser and starts working with it."
— Evan You, Creator of Vite and Vue.js
For new projects, Vite is the clear default. The developer experience improvement over Webpack and CRA is substantial enough that even teams with established Webpack setups are migrating. Start with npm create vite@latest and experience for yourself what a build tool feels like when it doesn't make you wait.