Vite (French for "fast") is a next-generation frontend build tool that dramatically improves the development experience. By leveraging native ES modules and esbuild, Vite offers instant server start and lightning-fast Hot Module Replacement (HMR). This guide covers using Vite for production projects from a senior developer's perspective.
Why Vite
Vite offers transformative advantages:
- Instant Server Start: No bundling during development
- Lightning HMR: Updates in milliseconds, not seconds
- Optimized Builds: Rollup-based production builds
- Framework Agnostic: Vue, React, Svelte, vanilla JS
- Modern Standards: Native ESM, TypeScript out of the box
Getting Started
Create New Project
# Interactive project creation
npm create vite@latest
# Or specify options
npm create vite@latest my-app -- --template vue
npm create vite@latest my-app -- --template react-ts
npm create vite@latest my-app -- --template vanilla
# Available templates
# vanilla, vanilla-ts, vue, vue-ts, react, react-ts,
# react-swc, react-swc-ts, preact, preact-ts,
# lit, lit-ts, svelte, svelte-ts, solid, solid-ts, qwik, qwik-ts
Project Structure
my-app/
├── public/ # Static assets (copied as-is)
│ └── favicon.ico
├── src/
│ ├── assets/ # Processed assets
│ │ └── logo.svg
│ ├── components/
│ ├── App.vue # or App.tsx
│ ├── main.js # Entry point
│ └── style.css
├── index.html # Entry HTML
├── package.json
└── vite.config.js # Vite configuration
Basic Commands
# Start dev server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
Configuration
vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
// Dev server options
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
},
// Build options
build: {
outDir: 'dist',
sourcemap: true,
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia']
}
}
}
},
// Path aliases
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils')
}
},
// CSS options
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
},
// Environment variables prefix
envPrefix: 'APP_'
});
Environment Variables
Create .env files:
# .env
VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My App
# .env.production
VITE_API_URL=https://api.production.com
# .env.development
VITE_API_URL=http://localhost:3001
Access in code:
// In JavaScript/TypeScript
const apiUrl = import.meta.env.VITE_API_URL;
const mode = import.meta.env.MODE; // 'development' or 'production'
const isDev = import.meta.env.DEV; // Boolean
const isProd = import.meta.env.PROD; // Boolean
// Type definitions (env.d.ts)
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Asset Handling
Importing Assets
// Import as URL
import imgUrl from './image.png';
// imgUrl = '/assets/image.abc123.png' in production
// Import as string (raw)
import shaderCode from './shader.glsl?raw';
// Import as worker
import Worker from './worker.js?worker';
const worker = new Worker();
// Dynamic imports
const modules = import.meta.glob('./modules/*.js');
// { './modules/a.js': () => import('./modules/a.js'), ... }
// Eager loading
const modules = import.meta.glob('./modules/*.js', { eager: true });
Public Directory
Files in public/ are served at the root and not processed:
<!--Access directly-->
<img src="/logo.png" />
<link rel="icon" href="/favicon.ico" />
CSS
// Import CSS
import './style.css';
// CSS Modules
import styles from './Component.module.css';
// <div className={styles.container}>
// Preprocessors (install sass, less, or stylus)
import './style.scss';
import './style.less';
TypeScript Support
Vite supports TypeScript out of the box:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Framework Examples
Vue 3
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()]
});
<!-- src/App.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import HelloWorld from './components/HelloWorld.vue';
const count = ref(0);
</script>
<template>
<HelloWorld msg="Vite + Vue" />
<button @click="count++">Count: {{ count }}</button>
</template>
<style scoped>
button {
padding: 0.5rem 1rem;
}
</style>
React
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()]
});
// src/App.tsx
import { useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<h1>Vite + React</h1>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
</div>
);
}
export default App;
Server-Side Rendering (SSR)
Basic SSR Setup
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
ssr: true
}
});
// server.js
import express from 'express';
import { createServer as createViteServer } from 'vite';
async function createServer() {
const app = express();
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom'
});
app.use(vite.middlewares);
app.use('*', async (req, res) => {
const url = req.originalUrl;
try {
let template = await vite.transformIndexHtml(url,
fs.readFileSync('index.html', 'utf-8')
);
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);
} catch (e) {
vite.ssrFixStacktrace(e);
console.error(e);
res.status(500).end(e.message);
}
});
app.listen(3000);
}
createServer();
Client-Only Imports
For libraries that don't work in SSR:
// Dynamic import for client only
const Modal = !import.meta.env.SSR
? (await import('bootstrap/js/dist/modal')).default
: null;
// Or use Vite's SSR option
if (!import.meta.env.SSR) {
import('client-only-library').then(lib => {
// Use library
});
}
Docker Integration
Dockerfile
# Build stage
FROM node:20-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Development with Docker
# docker-compose.yml
services:
frontend:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
environment:
- VITE_API_URL=http://api:8080
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev", "--", "--host"]
SSL in Development
// vite.config.js
import { defineConfig } from 'vite';
import fs from 'fs';
export default defineConfig({
server: {
https: {
key: fs.readFileSync('./certs/localhost.key'),
cert: fs.readFileSync('./certs/localhost.crt')
},
host: true,
port: 3000
}
});
Performance Optimization
Code Splitting
// Lazy-loading routes (Vue Router)
const routes = [
{ path: '/', component: () => import('./views/Home.vue') },
{ path: '/about', component: () => import('./views/About.vue') }
];
// React lazy
const About = React.lazy(() => import('./views/About'));
Build Analysis
# Install rollup-plugin-visualizer
npm install -D rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true
})
]
});
Chunk Configuration
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('vue') || id.includes('@vue')) {
return 'vue-vendor';
}
if (id.includes('lodash')) {
return 'lodash';
}
return 'vendor';
}
}
}
}
}
});
Common Plugins
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import legacy from '@vitejs/plugin-legacy';
import compression from 'vite-plugin-compression';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
vue(),
// Legacy browser support
legacy({
targets: ['defaults', 'not IE 11']
}),
// Gzip compression
compression({
algorithm: 'gzip',
ext: '.gz'
}),
// PWA support
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: 'My App',
short_name: 'App',
theme_color: '#ffffff'
}
})
]
});
Troubleshooting
404 for Routes in Production
Configure your server for SPA routing:
# nginx.conf
location / {
try_files $uri $uri/ /index.html;
}
HMR Not Working
// vite.config.js
export default defineConfig({
server: {
watch: {
usePolling: true // For Docker/WSL
},
hmr: {
host: 'localhost' // Or your dev host
}
}
});
Key Takeaways
- Native ESM: Development uses the browser's native module system
- esbuild for speed: Pre-bundling dependencies is instant
- Rollup for production: Optimized, tree-shaken bundles
- Framework plugins: First-class support for major frameworks
- Environment variables: Use the
VITE_ prefix for client exposure - SSR ready: Built-in support for server-side rendering
Vite transforms frontend development with its speed and simplicity—once you experience instant HMR, you won't want to go back.