Getting started
Installation
# Create new project with prompts
npm create vite@latest
# Create with specific template
npm create vite@latest my-app -- --template react
# Create with TypeScript
npm create vite@latest my-app -- --template react-ts
Available templates: vanilla, vanilla-ts, vue, vue-ts, react, react-ts, preact, preact-ts, lit, lit-ts, svelte, svelte-ts, solid, solid-ts, qwik, qwik-ts
Quick start
cd my-app
npm install # Install dependencies
npm run dev # Start dev server
npm run build # Production build
npm run preview # Preview prod build
Basic structure
my-app/
├── index.html # Entry HTML
├── package.json
├── vite.config.js # Config file
├── public/ # Static assets
└── src/
├── main.js # App entry
└── style.css
Dev server
CLI commands
vite # Start dev server
vite --host # Expose to network
vite --port 3000 # Custom port
vite --open # Auto-open browser
vite --https # Enable HTTPS
vite --force # Force re-optimize
Dev server config
// vite.config.js
export default {
server: {
port: 3000,
host: true, // Listen on all addresses
open: "/docs", // Auto-open path
https: true,
cors: true,
proxy: {
"/api": "http://localhost:4000",
},
},
};
Hot Module Replacement (HMR)
// Accept hot updates
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// Handle module update
});
}
// Dispose side effects
import.meta.hot.dispose((data) => {
// Cleanup
});
// Self-accept
import.meta.hot.accept();
Configuration
Basic config
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
root: './src', // Project root
base: '/my-app/', // Public base path
publicDir: 'public', # Static assets dir
build: {
outDir: '../dist', # Output directory
assetsDir: 'assets' # Assets subdirectory
}
})
Config with TypeScript
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": "/src",
"@components": "/src/components",
},
},
});
Conditional config
export default defineConfig(({ command, mode }) => {
if (command === "serve") {
// Dev config
return {
server: { port: 3000 },
};
} else {
// Build config
return {
build: { minify: "esbuild" },
};
}
});
Asset handling
Import assets
// Import as URL
import imgUrl from "./img.png";
// Import as string (text files)
import txt from "./file.txt?raw";
// Import as Web Worker
import Worker from "./worker.js?worker";
// Import JSON
import data from "./data.json";
Static assets
// Files in /public are served at /
<img src="/logo.png" />;
// Import from src/ gets hashed URL
import logo from "./assets/logo.png";
<img src={logo} />;
URL suffixes
// Explicit URL import
import url from "./asset.png?url";
// Force raw string
import raw from "./file.txt?raw";
// Import as inline base64
import inline from "./data.svg?inline";
// Web Worker
import MyWorker from "./worker?worker";
const worker = new MyWorker();
CSS & preprocessors
Import CSS
// Import global CSS
import "./style.css";
// CSS modules
import styles from "./style.module.css";
<div className={styles.red} />;
// Import SCSS
import "./style.scss";
// Import Less
import "./style.less";
CSS Modules
/* style.module.css */
.red {
color: red;
}
import styles from './style.module.css'
// Access as property
<div className={styles.red} />
// Multiple classes
<div className={`${styles.red} ${styles.bold}`} />
PostCSS
// vite.config.js
export default {
css: {
postcss: {
plugins: [require("autoprefixer"), require("postcss-nested")],
},
},
};
Or create postcss.config.js:
module.exports = {
plugins: {
autoprefixer: {},
"postcss-nested": {},
},
};
Preprocessors
# Install preprocessor
npm install -D sass
npm install -D less
npm install -D stylus
// Auto-detected by file extension
import "./style.scss";
import "./style.less";
import "./style.styl";
Build
Production build
vite build # Build for production
vite build --mode staging # Custom mode
vite build --base=/app/ # Custom base path
vite build --outDir dist # Custom output
vite build --watch # Watch mode
Build config
export default {
build: {
target: 'es2015', # Browser target
outDir: 'dist', # Output directory
assetsDir: 'assets', # Assets subdirectory
assetsInlineLimit: 4096, # Inline < 4kb
cssCodeSplit: true, # Split CSS
sourcemap: true, # Generate sourcemaps
minify: 'esbuild', # esbuild | terser
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom']
}
}
}
}
}
Preview build
vite preview # Preview production build
vite preview --port 4173 # Custom port
vite preview --open # Auto-open browser
Bundle analysis
// vite.config.js
import { visualizer } from "rollup-plugin-visualizer";
export default {
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
],
};
Plugins
Official plugins
# React with Fast Refresh
npm install -D @vitejs/plugin-react
# Vue 3 with SFC support
npm install -D @vitejs/plugin-vue
# Vue JSX
npm install -D @vitejs/plugin-vue-jsx
# Legacy browser support
npm install -D @vitejs/plugin-legacy
Using plugins
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import legacy from "@vitejs/plugin-legacy";
export default defineConfig({
plugins: [
react({
fastRefresh: true,
jsxRuntime: "automatic",
}),
legacy({
targets: ["defaults", "not IE 11"],
}),
],
});
Common community plugins
// Image optimization
import imagemin from "vite-plugin-imagemin";
// SVG as React components
import svgr from "vite-plugin-svgr";
// PWA
import { VitePWA } from "vite-plugin-pwa";
export default {
plugins: [imagemin(), svgr(), VitePWA({ registerType: "autoUpdate" })],
};
Environment variables
.env files
# .env - All environments
VITE_API_URL=https://api.example.com
# .env.development - Dev only
VITE_API_URL=http://localhost:3000
# .env.production - Prod only
VITE_API_URL=https://api.prod.com
# .env.local - Local overrides (gitignored)
VITE_SECRET_KEY=abc123
⚠️ Only VITE_ prefixed vars are exposed to client code
Access variables
// In application code
const apiUrl = import.meta.env.VITE_API_URL
const mode = import.meta.env.MODE
const isDev = import.meta.env.DEV
const isProd = import.meta.env.PROD
// TypeScript IntelliSense
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
}
Define variables
// vite.config.js
export default {
define: {
__APP_VERSION__: JSON.stringify("1.0.0"),
__API_URL__: JSON.stringify(process.env.API_URL),
},
};
SSR (Server-Side Rendering)
Basic SSR setup
// server.js
import express from "express";
import { createServer as createViteServer } from "vite";
const app = express();
const vite = await createViteServer({
server: { middlewareMode: true },
});
app.use(vite.middlewares);
app.use("*", async (req, res) => {
const url = req.originalUrl;
// Load template
let template = fs.readFileSync("index.html", "utf-8");
template = await vite.transformIndexHtml(url, template);
// Render app
const { render } = await vite.ssrLoadModule("/src/entry-server.js");
const appHtml = await render(url);
const html = template.replace("<!--ssr-outlet-->", appHtml);
res.status(200).set({ "Content-Type": "text/html" }).end(html);
});
app.listen(3000);
SSR config
// vite.config.js
export default {
ssr: {
noExternal: ['package-name'], # Force bundle
external: ['fsevents'], # Exclude
target: 'node' # node | webworker
}
}
Build for SSR
# Build client
vite build --outDir dist/client
# Build server
vite build --ssr src/entry-server.js --outDir dist/server
Library mode
Build as library
// vite.config.js
import { resolve } from "path";
export default {
build: {
lib: {
entry: resolve(__dirname, "lib/main.js"),
name: "MyLib",
fileName: (format) => `my-lib.${format}.js`,
formats: ["es", "cjs", "umd", "iife"],
},
rollupOptions: {
// Externalize deps that shouldn't be bundled
external: ["vue", "react"],
output: {
globals: {
vue: "Vue",
react: "React",
},
},
},
},
};
Library package.json
{
"name": "my-lib",
"type": "module",
"files": ["dist"],
"main": "./dist/my-lib.cjs.js",
"module": "./dist/my-lib.es.js",
"exports": {
".": {
"import": "./dist/my-lib.es.js",
"require": "./dist/my-lib.cjs.js"
}
}
}
Performance optimization
Code splitting
// Dynamic imports
const module = await import("./module.js");
// React lazy loading
const Component = lazy(() => import("./Component"));
// Manual chunks
export default {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
return "vendor";
}
},
},
},
},
};
Dependency pre-bundling
export default {
optimizeDeps: {
include: ['linked-dep'], # Force include
exclude: ['your-package'], # Don't pre-bundle
entries: ['./src/main.js'], # Entry points
esbuildOptions: {
target: 'es2020'
}
}
}
Build optimizations
export default {
build: {
minify: 'esbuild', # Faster than terser
cssCodeSplit: true, # Split CSS by chunk
assetsInlineLimit: 4096, # Inline small assets
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
compact: true,
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['@mui/material']
}
}
}
}
}
Webpack migration tips
webpack.config.js → vite.config.js
| Webpack | Vite |
|---|---|
entry |
build.rollupOptions.input |
output.path |
build.outDir |
output.publicPath |
base |
resolve.alias |
resolve.alias |
module.rules |
Native or plugins |
plugins |
plugins (different ecosystem) |
devServer.proxy |
server.proxy |
mode |
mode (from CLI or config) |
optimization.splitChunks |
build.rollupOptions.output |
Common patterns
// webpack: require() → Vite: import
// ❌ const img = require('./img.png')
✅ import img from './img.png'
// webpack: require.context → Vite: import.meta.glob
// ❌ require.context('./dir', true, /\.js$/)
✅ const modules = import.meta.glob('./dir/**/*.js')
// webpack: process.env.* → Vite: import.meta.env.VITE_*
// ❌ const api = process.env.API_URL
✅ const api = import.meta.env.VITE_API_URL
// webpack: DefinePlugin → Vite: define
// ❌ new webpack.DefinePlugin({ ... })
✅ export default { define: { ... } }
Advanced patterns
Glob imports
// Eager import all modules
const modules = import.meta.glob("./dir/*.js", { eager: true });
// Lazy import with await
const modules = import.meta.glob("./dir/*.js");
const mod = await modules["./dir/foo.js"]();
// Import as raw string
const files = import.meta.glob("./dir/*.md", {
as: "raw",
eager: true,
});
// Custom queries
const urls = import.meta.glob("./dir/*.svg", {
as: "url",
eager: true,
});
Multi-page app
import { resolve } from "path";
export default {
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
nested: resolve(__dirname, "nested/index.html"),
admin: resolve(__dirname, "admin/index.html"),
},
},
},
};
Custom server middleware
export default {
server: {
middlewareMode: false,
},
plugins: [
{
name: "custom-middleware",
configureServer(server) {
server.middlewares.use((req, res, next) => {
// Custom logic
next();
});
},
},
],
};
Debugging & troubleshooting
Debug mode
# Enable debug logs
DEBUG=vite:* vite
# Specific module
DEBUG=vite:deps vite
# Clear cache and restart
vite --force
Common issues
# "Unexpected token" errors
# Fix: Add to optimizeDeps.include
# Dependency pre-bundling issues
# Fix: Use --force flag or delete node_modules/.vite
# CORS errors in dev
# Fix: Configure server.cors in vite.config.js
# Large bundle size
# Fix: Use rollup-plugin-visualizer to analyze
Config resolution
// Log resolved config
export default defineConfig(({ command, mode }) => {
console.log('Command:', command) # serve | build
console.log('Mode:', mode) # development | production
return {
// config
}
})
Gotchas
⚠️ Environment variables - Only VITE_ prefixed vars are exposed to client. Don't use for secrets!
⚠️ index.html is entry point - Unlike webpack, index.html is at project root and imports /src/main.js
⚠️ No require() - Use ESM import instead. Use import.meta.glob() instead of require.context()
⚠️ Public directory - Files in /public are served as-is at /. Don't import them in code, reference by absolute path
⚠️ JSON imports - Named imports work: import { field } from './data.json'
⚠️ CSS Modules - Must use .module.css extension, not configurable like webpack
⚠️ Dynamic imports with variables - Must use import.meta.glob() for dynamic patterns, can't use template literals in import()
⚠️ SSR external packages - Some packages need ssr.noExternal to work properly in SSR
⚠️ Base path - When deploying to subpath, must set base: '/my-app/' in config
Also see
- Vite official docs (vitejs.dev)
- Vite GitHub (github.com)
- Awesome Vite (github.com)
- Vite plugin directory (vitejs.dev)
- Vite Rollup plugins (vite-rollup-plugins.patak.dev)
- Migration from webpack (vitejs.dev)