Vite is the fast, modern build tool that powers Vue, React, Svelte, and SvelteKit projects. This reference covers everything from vite.config.ts setup to production build optimisation.
Quick reference
The 25 Vite patterns you'll use every day.
| Pattern | What it does |
|---|---|
npm create vite@latest |
Scaffold a new project |
vite / vite dev |
Start dev server |
vite build |
Production build |
vite preview |
Preview production build locally |
vite --port 3000 |
Custom port |
vite --host |
Expose to LAN |
defineConfig({}) |
Type-safe config helper |
resolve.alias |
Path aliases (@/ → src/) |
server.proxy |
Proxy API requests |
build.outDir |
Change output directory |
build.rollupOptions |
Custom Rollup options |
build.sourcemap |
Enable source maps |
build.minify |
Minifier (esbuild/terser) |
plugins: [react()] |
Add plugin |
import.meta.env.VITE_X |
Read env variable |
import.meta.hot |
HMR API |
?raw suffix |
Import as string |
?url suffix |
Import as URL |
?worker suffix |
Import as Web Worker |
?inline suffix |
Inline as base64 |
optimizeDeps.include |
Force pre-bundle a dep |
optimizeDeps.exclude |
Skip pre-bundling |
css.modules |
CSS Modules config |
assetsInclude |
Treat extra file types as assets |
envPrefix |
Change env variable prefix |
Installation and scaffolding
# Interactive scaffold (choose React/Vue/Svelte/Lit/Vanilla + JS/TS)
npm create vite@latest my-app
cd my-app && npm install && npm run dev
# Direct template flags
npm create vite@latest my-app -- --template react-ts
npm create vite@latest my-app -- --template vue-ts
npm create vite@latest my-app -- --template svelte-ts
npm create vite@latest my-app -- --template vanilla-ts
# Vite CLI
npm run dev # vite
npm run build # vite build
npm run preview # vite preview
vite.config.ts structure
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@lib': resolve(__dirname, 'src/lib'),
},
},
server: {
port: 5173,
host: true, // expose to LAN
open: true, // open browser on start
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
build: {
outDir: 'dist',
sourcemap: true,
minify: 'esbuild', // 'terser' for smaller output
target: 'es2020',
},
})
Path aliases
// vite.config.ts
import { resolve } from 'path'
export default defineConfig({
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
})
// tsconfig.json — must match vite.config.ts aliases
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
// usage in your code
import { Button } from '@/components/Button'
import { formatDate } from '@/lib/utils'
Environment variables
Vite exposes variables prefixed with VITE_ to the client.
# .env (shared, committed)
VITE_APP_TITLE=My App
VITE_API_URL=https://api.example.com
# .env.local (private, git-ignored)
VITE_SECRET_KEY=dev-only-secret
# .env.production
VITE_API_URL=https://api.production.com
# .env.development
VITE_API_URL=http://localhost:3001
// Access in code
const title = import.meta.env.VITE_APP_TITLE
const apiUrl = import.meta.env.VITE_API_URL
const mode = import.meta.env.MODE // 'development' | 'production'
const isProd = import.meta.env.PROD // boolean
const isDev = import.meta.env.DEV // boolean
// TypeScript autocomplete — create env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Never put secrets in
VITE_variables — they end up in the client bundle.
Dev server configuration
export default defineConfig({
server: {
port: 3000,
strictPort: true, // fail if port is in use
host: '0.0.0.0', // expose on all interfaces
open: '/dashboard', // open specific path on start
https: {}, // enable HTTPS (self-signed cert)
cors: true, // enable CORS for dev server
proxy: {
// Simple string shorthand
'/api': 'http://localhost:3001',
// Full options
'/auth': {
target: 'http://localhost:3001',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/auth/, '/v1/auth'),
},
// WebSocket proxy
'/ws': {
target: 'ws://localhost:3001',
ws: true,
},
},
watch: {
ignored: ['**/node_modules/**', '**/dist/**'],
},
},
})
Build configuration
export default defineConfig({
build: {
outDir: 'dist',
assetsDir: 'assets', // subdirectory for assets
assetsInlineLimit: 4096, // inline assets < 4KB as base64
sourcemap: true, // 'inline' | 'hidden' | boolean
minify: 'esbuild', // 'terser' = smaller, 'esbuild' = faster
target: 'es2020', // transpile target
cssMinify: true,
reportCompressedSize: true,
// Code splitting
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
},
},
},
// Library mode (publishing a package)
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyLib',
formats: ['es', 'cjs'],
fileName: (format) => `my-lib.${format}.js`,
},
},
})
Plugins
Official plugins
npm i -D @vitejs/plugin-react # React + Fast Refresh
npm i -D @vitejs/plugin-react-swc # React + SWC (faster)
npm i -D @vitejs/plugin-vue # Vue 3 SFCs
npm i -D @vitejs/plugin-vue-jsx # Vue 3 JSX
npm i -D @vitejs/plugin-legacy # Legacy browser support
import react from '@vitejs/plugin-react'
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
react(),
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
})
Community plugins
npm i -D vite-plugin-svgr # Import SVGs as React components
npm i -D vite-tsconfig-paths # Auto path aliases from tsconfig.json
npm i -D @vitejs/plugin-basic-ssl # HTTPS with one line
npm i -D unplugin-icons # 150k+ icons on demand
npm i -D vite-plugin-checker # TypeScript/ESLint errors in overlay
import svgr from 'vite-plugin-svgr'
import tsconfigPaths from 'vite-tsconfig-paths'
import checker from 'vite-plugin-checker'
export default defineConfig({
plugins: [
react(),
svgr(), // import Logo from './logo.svg?react'
tsconfigPaths(), // reads tsconfig paths automatically
checker({ typescript: true }), // shows TS errors in browser overlay
],
})
Static assets
// Import as URL (hashed, copied to dist)
import imgUrl from './logo.png'
// → imgUrl = '/assets/logo-abc123.png'
// Import as raw string
import svgString from './icon.svg?raw'
// Import as inline base64
import inlineImg from './small.png?inline'
// Import as Web Worker
import MyWorker from './worker.ts?worker'
const worker = new MyWorker()
// Import as Shared Worker
import SharedWorker from './shared.ts?sharedworker'
// Import as URL without bundling
import wasmUrl from './module.wasm?url'
// vite.config.ts — treat extra extensions as assets
export default defineConfig({
assetsInclude: ['**/*.gltf', '**/*.wasm'],
})
CSS
export default defineConfig({
css: {
// CSS Modules options
modules: {
localsConvention: 'camelCase', // 'camelCase' | 'dashes' | 'camelCaseOnly'
scopeBehaviour: 'local',
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
// PostCSS config (or put in postcss.config.js)
postcss: {
plugins: [
// autoprefixer(), tailwindcss(), etc.
],
},
// Sass/Less/Stylus preprocessor options
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables" as *;`,
},
},
devSourcemap: true,
},
})
// CSS Modules usage
import styles from './Button.module.css'
<button className={styles.primary}>Click</button>
// Tailwind CSS — just install and add to postcss
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Hot Module Replacement (HMR)
// Manual HMR API (for non-framework code)
if (import.meta.hot) {
import.meta.hot.accept('./dep.ts', (newModule) => {
// handle updated module
if (newModule) {
newModule.doSomething()
}
})
// Accept self-updates
import.meta.hot.accept(() => {
// re-run module
})
// Dispose side effects
import.meta.hot.dispose((data) => {
data.savedState = currentState
clearInterval(timer)
})
// Restore state after hot reload
const state = import.meta.hot.data.savedState ?? initialState
}
Optimising dependencies
export default defineConfig({
optimizeDeps: {
// Force pre-bundle (useful for CJS packages or monorepos)
include: [
'lodash-es',
'date-fns',
'@org/shared-pkg > internal-dep',
],
// Skip pre-bundling (e.g. already ESM, or has issues)
exclude: ['some-esm-package'],
// Keep pre-bundled even when unused
holdUntilCrawlEnd: false,
},
})
Multi-page app (MPA)
import { resolve } from 'path'
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
about: resolve(__dirname, 'about/index.html'),
dashboard: resolve(__dirname, 'dashboard/index.html'),
},
},
},
})
Common patterns
Conditional config by mode
export default defineConfig(({ command, mode }) => {
const isDev = command === 'serve'
const isProd = command === 'build'
const isTest = mode === 'test'
return {
plugins: [
react(),
isProd && someProdOnlyPlugin(),
].filter(Boolean),
build: {
sourcemap: isDev,
},
}
})
Environment-aware API base URL
// src/lib/api.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? '/api'
export async function fetchUser(id: string) {
const res = await fetch(`${BASE_URL}/users/${id}`)
if (!res.ok) throw new Error(res.statusText)
return res.json()
}
Loading .env variables in vite.config.ts itself
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
// Load env file from project root
const env = loadEnv(mode, process.cwd(), '')
return {
define: {
__APP_VERSION__: JSON.stringify(env.npm_package_version),
},
}
})
Global constants with define
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify('1.2.0'),
__FEATURE_FLAGS__: JSON.stringify({ darkMode: true }),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
})
Common mistakes
| Mistake | Fix |
|---|---|
Putting secrets in VITE_ env vars |
Use server-side env vars; VITE_ is public |
Using __dirname without config |
import { resolve } from 'path' + resolve(__dirname, …) in config |
Forgetting tsconfig.json paths when adding alias |
Alias in vite.config.ts and paths in tsconfig.json must match |
optimizeDeps not picking up a CJS dep |
Add it to optimizeDeps.include |
| CSS not scoped in production | Use .module.css or a CSS-in-JS solution |
| HMR full reload instead of partial | Ensure module uses import.meta.hot.accept |
| Build output too large | Add manualChunks in rollupOptions.output |
.env.local committed to git |
Add .env*.local to .gitignore |
Vite vs alternatives
| Feature | Vite | webpack | Parcel | esbuild |
|---|---|---|---|---|
| Dev server speed | ⚡ Instant (ESM) | 🐢 Bundle first | 🐢 Bundle first | ⚡ Fast |
| HMR speed | ⚡ Module-level | 🐢 Full bundle | Medium | Manual |
| Config complexity | Low | High | Zero | Low |
| Plugin ecosystem | Growing | Huge | Small | Small |
| Build output | Rollup (optimised) | webpack | Parcel | Single file |
| SSR support | Yes | Yes | Limited | Manual |
| TypeScript | Native | Via loader | Native | Native |
| CSS Modules | Built-in | Via loader | Built-in | Manual |
FAQ
Why does Vite use esbuild for dev but Rollup for build?
esbuild is 10–100× faster than Rollup but has a less complete plugin system and output optimisation. Vite uses esbuild for instant dev server startup and Rollup for production builds where bundle quality matters more than speed.
How do I use Vite with an existing webpack project?
Incrementally: add Vite for new pages/packages while keeping webpack for existing ones. Most webpack concepts map to Vite — loaders become plugins, resolve.alias stays the same name. The main gotcha is CJS vs ESM — Vite requires ESM source.
What does ?url, ?raw, ?inline mean?
These are Vite's import query suffixes that control how an asset is processed. ?url gives you a URL string, ?raw gives you the file content as a string, ?inline inlines the file as a base64 data URL. Without a suffix, Vite processes the file normally (images become URLs, JS is bundled, etc.).
How do I fix "cannot find module" for a path alias in tests (Vitest)?
Add the same resolve.alias to your vitest.config.ts (or the test key in vite.config.ts). Vitest uses Vite's resolver, but it reads its own config separately unless you use defineConfig from vitest/config.
How do I deploy a Vite app?
Run vite build, then serve the dist/ folder as static files. For SPA routing, configure your server to return index.html for all 404s. On Netlify: add _redirects file with /* /index.html 200. On Vercel: add vercel.json with a rewrite rule.
Can Vite build a library (not an app)?
Yes — set build.lib in vite.config.ts with an entry point and output formats (es, cjs, umd). Set build.rollupOptions.external to exclude peer dependencies like React from the bundle. This is the standard approach for publishing npm packages with Vite.