Vite
Vite is a next-generation build tool that uses native ESM for blazing fast development and Rollup for optimized production builds. It's the modern default for React, Vue, and Svelte projects.
Why Vite?
Traditional bundlers like Webpack bundle your entire app before the dev server starts. As apps grow, this gets slow.
Vite's insight: modern browsers natively support ES modules. In development, serve files directly as ESM โ no bundling needed! The browser handles imports itself, and the dev server only processes what the browser requests.
Traditional (Webpack dev mode):
Start โ Bundle everything โ Dev server ready (30s+ on large apps)
Edit file โ Re-bundle affected modules โ Update (1-5s)
Vite:
Start โ Dev server ready instantly (< 1s)
Edit file โ Only transform that one file โ HMR update (< 100ms)
Getting Started
npm create vite@latest my-app -- --template react
# or: react-ts, vue, vue-ts, svelte, vanilla, etc.
cd my-app
npm install
npm run dev
Default project structure:
my-app/
index.html โ Vite entry point (in root, not public/)
public/ โ static assets (served as-is)
src/
main.jsx โ JavaScript entry
App.jsx
App.css
vite.config.js
package.json
How Vite Works in Development
- You start
vitedev server - Browser requests
index.html - Vite serves
index.htmlwith a<script type="module">referencing your entry - Browser sees
import App from './App.jsx'โ requests/src/App.jsx - Vite transforms the file on request (JSX โ JS, TypeScript โ JS)
- Browser fetches dependencies recursively
Only imported modules are processed. Unused code is never touched.
Pre-bundling (esbuild): Vite bundles node_modules upfront with esbuild (written in Go, 10-100x faster than webpack). This converts CJS packages to ESM and bundles packages with many internal imports into one.
vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
port: 3000,
open: true,
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ""),
},
},
},
build: {
outDir: "dist",
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"], // separate vendor chunk
},
},
},
},
css: {
modules: {
localsConvention: "camelCase", // CSS Module class names
},
},
});
Hot Module Replacement
Vite's HMR is fast and precise โ only the changed module and its direct importers are updated, without losing application state:
// src/counter.js
export let count = 0;
export function increment() { count++; }
// Vite HMR API
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// Handle update
});
}
With React plugin, HMR is handled automatically via React Refresh โ components update in-place without losing state.
Environment Variables
# .env
VITE_API_URL=http://localhost:8080
VITE_APP_NAME=MyApp
// Access in JS (must be prefixed with VITE_)
console.log(import.meta.env.VITE_API_URL);
console.log(import.meta.env.MODE); // "development" or "production"
console.log(import.meta.env.DEV); // true in development
console.log(import.meta.env.PROD); // true in production
Only VITE_-prefixed variables are exposed to client code (unlike Next.js which uses NEXT_PUBLIC_).
Static Assets
// Import as URL (returns a string path)
import logo from "./logo.svg";
console.log(logo); // "/assets/logo.a3b2c1.svg"
// Import as raw string
import shaderCode from "./shader.glsl?raw";
// Import as URL explicitly
import workerUrl from "./heavy-work.js?url";
// Import as web worker
import MyWorker from "./worker.js?worker";
const worker = new MyWorker();
Vite vs Webpack: Side by Side
| Feature | Vite | Webpack 5 |
|---|---|---|
| Dev server startup | < 1s | 10-60s |
| HMR | < 100ms | 1-5s |
| Config complexity | Simple | Complex |
| Ecosystem | Growing | Massive |
| Production bundler | Rollup | Webpack |
| CJS support | Via pre-bundling | Native |
| Framework support | Plugins for React/Vue/Svelte | Loaders |
Production Build
For production, Vite uses Rollup (not the dev server approach):
npm run build # outputs to dist/
npm run preview # preview the production build locally
The production output is a fully bundled, tree-shaken, minified set of files โ similar to what Webpack produces. Both development speed and production optimization.
Vite Plugins
The plugin ecosystem is growing:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr"; // SVG as React components
import { VitePWA } from "vite-plugin-pwa"; // PWA support
import tsconfigPaths from "vite-tsconfig-paths"; // TypeScript paths
export default defineConfig({
plugins: [
react(),
svgr(),
tsconfigPaths(),
VitePWA({ registerType: "autoUpdate" }),
],
});
Key Takeaways
- Vite serves files directly as ESM in development โ no bundling, near-instant startup
- Pre-bundles
node_moduleswith esbuild (very fast) to convert CJS โ ESM - Production builds use Rollup โ optimized tree-shaking and output
- HMR updates are precise and don't reset component state
- Environment variables must be prefixed with
VITE_to be exposed vite.config.jsis simpler than webpack configs, but still fully customizable
Ready to test your knowledge?
Take a quiz on what you just learned.