13 - Vite β
What is Vite β
Vite (French for "fast") is a build tool and dev server for modern frontend projects. Created by Evan You (creator of Vue). Framework-agnostic β works with React, Vue, Svelte, vanilla JS, etc.
Two main jobs:
- Dev server β serves files using native ES modules, instant startup
- Production build β bundles using Rollup under the hood
Why Vite Over Webpack β
The Problem with Webpack β
Webpack dev server startup:
1. Read ALL files in the project
2. Bundle EVERYTHING into memory
3. Apply ALL loaders and transforms
4. Serve the bundle
Time: 10-60 seconds (grows with project size)
When you edit a file:
1. Re-bundle the affected modules + dependencies
2. Send entire updated bundle to browser
Time: 1-10 seconds (HMR, but still reprocesses a lot)How Vite Solves This β
Vite dev server startup:
1. Pre-bundle node_modules with esbuild (one-time, cached)
2. That's it. No bundling of YOUR code.
Time: ~300ms (regardless of project size)
When you edit a file:
1. Only the changed file is re-transformed
2. Browser fetches ONLY that file via ESM import
Time: <50ms (instant HMR)Key insight: Vite doesn't bundle your code during development. The browser loads files individually using native ES module import statements.
How Vite Works Internally β
Dev Mode: Native ESM β
Your code:
import { Button } from './components/Button'
Traditional bundler:
Resolves import β reads Button.tsx β transforms β bundles into app.js β serves
Vite:
Browser requests /src/App.tsx
Vite transforms it on-the-fly (JSX β JS, TS β JS)
Browser sees: import { Button } from '/src/components/Button.tsx'
Browser requests /src/components/Button.tsx
Vite transforms that file too
Each file = one HTTP request (HTTP/2 makes this fast)βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser β
β β
β <script type="module" src="/src/main.tsx"> β
β β β
β βββ import App from './App.tsx' β
β β βββ import Button from './Button' β
β β βββ import hooks from './hooks' β
β βββ import './index.css' β
β β
β Each import = HTTP request to Vite dev server β
β Vite transforms on demand (only what's needed) β
βββββββββββββββββββββββββββββββββββββββββββββββββββDependency Pre-Bundling (esbuild) β
Node modules are NOT ES modules (many are CommonJS). Vite can't serve them as-is.
Solution: On first run, Vite pre-bundles all node_modules dependencies using esbuild (10-100x faster than Webpack's JS-based bundlers).
First run:
node_modules/react/ (1000+ files, CommonJS)
β esbuild pre-bundle
node_modules/.vite/deps/react.js (single ESM file, cached)
Subsequent runs:
Reads from cache β instant
Cache invalidated only when package.json or lock file changesProduction Build: Rollup β
Vite uses Rollup for production builds because:
- Native ESM in production would mean hundreds of HTTP requests (slow)
- Rollup has better tree shaking than esbuild
- Rollup has a mature plugin ecosystem
- Rollup produces smaller, more optimized bundles
npm run build
# Runs: rollup (via vite build)
# Output: dist/
# βββ index.html
# βββ assets/
# β βββ index-a1b2c3.js (your code, hashed)
# β βββ vendor-d4e5f6.js (node_modules, hashed)
# β βββ index-g7h8i9.css (extracted CSS, hashed)Hot Module Replacement (HMR) β
What: When you edit a file, only that module is replaced in the browser without a full page reload. State is preserved.
You edit Button.tsx:
1. Vite detects file change (chokidar file watcher)
2. Transforms only Button.tsx
3. Sends WebSocket message to browser: "Button.tsx changed"
4. Browser fetches new Button.tsx
5. React Fast Refresh swaps the component
6. NO page reload, state preservedVite's HMR is file-level, not module-graph level. Webpack recalculates the entire affected module chain. Vite just re-serves the one changed file.
React Fast Refresh (via @vitejs/plugin-react):
- Preserves component state during edits
- Only re-renders the changed component, not the whole tree
- Falls back to full reload if the edit changes non-component code (hooks, utils)
vite.config.ts β Anatomy β
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
// ---- Plugins ----
plugins: [
react(), // React Fast Refresh + JSX transform
],
// ---- Dev Server ----
server: {
port: 3000, // dev server port
open: true, // open browser on start
proxy: { // proxy API calls to backend
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
// ---- Build ----
build: {
outDir: 'dist', // output directory
sourcemap: true, // generate source maps
target: 'es2020', // browser target
rollupOptions: {
output: {
manualChunks: { // custom code splitting
react: ['react', 'react-dom'],
},
},
},
},
// ---- Resolve ----
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // @ = src/
},
},
// ---- CSS ----
css: {
modules: {
localsConvention: 'camelCase', // CSS modules: .my-class β myClass
},
},
// ---- Testing (Vitest) ----
test: {
globals: true, // no need to import describe/it/expect
environment: 'jsdom', // DOM simulation
setupFiles: './src/test-setup.ts',
},
});Environment Variables β
# .env β all environments
# .env.local β local overrides (gitignored)
# .env.development β dev only
# .env.production β production only# .env
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App// Access in code β MUST be prefixed with VITE_
const apiUrl = import.meta.env.VITE_API_URL;
const mode = import.meta.env.MODE; // 'development' or 'production'
const isDev = import.meta.env.DEV; // true in dev
const isProd = import.meta.env.PROD; // true in prodTypeScript support β add to src/vite-env.d.ts:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}How CSS Works in Vite β
Plain CSS β
import './styles.css'; // injected as <style> tag in dev, extracted in buildCSS Modules β
import styles from './Button.module.css';
// styles.primary β "Button_primary_a1b2c" (scoped class name)
<button className={styles.primary}>Click</button>PostCSS β
Vite reads postcss.config.js automatically. No extra config needed.
CSS Pre-processors β
npm install -D sass # for .scss files
npm install -D less # for .less filesJust install and import β Vite handles the rest.
Static Assets β
// Importing images β returns the URL
import logo from './assets/logo.png';
<img src={logo} alt="Logo" />
// In dev: /src/assets/logo.png
// In build: /assets/logo-a1b2c3.png (hashed for cache busting)
// Public directory β served as-is, no processing
// public/favicon.ico β accessible at /favicon.icoAsset size threshold: Files < 4KB are inlined as base64 data URLs (fewer HTTP requests). Files >= 4KB are copied to dist/assets/ with hash.
Configure:
build: {
assetsInlineLimit: 4096, // bytes (default 4KB)
}Common Vite Interview Questions β
Q: Why is Vite faster than Webpack in development? A: Vite doesn't bundle during dev. It serves files as native ES modules β the browser loads them individually. Only changed files are re-transformed. Webpack bundles everything upfront and re-bundles on change.
Q: What does Vite use under the hood? A: esbuild for dependency pre-bundling (CJSβESM, fast), Rollup for production builds (optimized output, tree shaking). In dev, Vite itself serves files with on-demand transforms.
Q: Why not use esbuild for production builds? A: esbuild is fast but its code splitting and CSS handling are less mature than Rollup's. Rollup produces smaller, better-optimized bundles. The Vite team is working on Rolldown (Rust-based Rollup replacement) for future versions.
Q: How does HMR work in Vite? A: File watcher detects change β Vite re-transforms only that file β sends WebSocket message to browser β browser fetches new module β React Fast Refresh swaps the component without losing state.
Q: What is dependency pre-bundling? A: node_modules packages are often CommonJS (not ESM). Vite pre-bundles them into single ESM files using esbuild on first run, then caches the result. This also reduces the number of HTTP requests (react = 1000+ files β 1 file).