Vite
Platform: Web only. Mobile demos use Expo with Metro bundler. See the expo-sdk skill.
Overview
Build tool and development server patterns for Vite 7.x. Provides instant server start, lightning-fast HMR, optimized production builds, and extensive plugin ecosystem with first-class TypeScript support.
Install: pnpm add -D vite
Workflows
Initial setup:
- • Create
vite.config.tswith TypeScript types - • Install React plugin:
pnpm add -D @vitejs/plugin-react - • Configure path aliases for clean imports
- • Set up environment variables with
.envfiles - • Test dev server:
pnpm vite
Production optimization:
- • Configure build output directory and asset handling
- • Set up code splitting and chunk optimization
- • Enable build compression (gzip/brotli)
- • Configure minification options
- • Run production build:
pnpm vite build - • Preview build locally:
pnpm vite preview
Basic Configuration
Minimal vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true
},
build: {
outDir: 'dist'
}
});
TypeScript-Aware Config
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@utils': path.resolve(__dirname, './src/utils'),
'@types': path.resolve(__dirname, './src/types')
}
}
});
Update tsconfig.json paths to match:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@hooks/*": ["./src/hooks/*"],
"@utils/*": ["./src/utils/*"],
"@types/*": ["./src/types/*"]
}
}
}
React Plugin Setup
Basic React Plugin
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
// Babel plugins for React (optional)
babel: {
plugins: [
// Add custom babel plugins here
]
}
})
]
});
Note: Fast Refresh is enabled by default in @vitejs/plugin-react. No configuration needed.
React with SWC (Faster Alternative)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
export default defineConfig({
plugins: [
react({
// SWC plugins
plugins: [
// Add SWC plugins here
]
})
]
});
Environment Variables
.env File Structure
# .env - Base config (committed) VITE_APP_NAME=Demo Harness VITE_API_VERSION=v1 # .env.local - Local overrides (gitignored) VITE_API_URL=http://localhost:3000 # .env.development - Dev defaults VITE_DEBUG=true VITE_API_URL=http://dev.example.com # .env.production - Production defaults VITE_DEBUG=false VITE_API_URL=https://api.example.com
CRITICAL: All env vars must start with VITE_ to be exposed to client code.
Using Environment Variables
// ✅ Accessing env vars in code
const apiUrl = import.meta.env.VITE_API_URL;
const isDev = import.meta.env.DEV;
const isProd = import.meta.env.PROD;
const mode = import.meta.env.MODE; // 'development' | 'production'
// Type-safe env vars
interface ImportMetaEnv {
readonly VITE_APP_NAME: string;
readonly VITE_API_URL: string;
readonly VITE_API_VERSION: string;
readonly VITE_DEBUG: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// ❌ NEVER commit secrets to .env files
// Use .env.local for API keys and credentials
Configuring Environment Variables
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
// Load env file based on mode
const env = loadEnv(mode, process.cwd(), '');
return {
define: {
// Expose non-VITE_ prefixed vars
__APP_VERSION__: JSON.stringify(env.npm_package_version)
},
server: {
port: Number(env.PORT) || 5173
}
};
});
Development Server
Basic Server Configuration
export default defineConfig({
server: {
port: 5173,
strictPort: true, // Exit if port is already in use
open: true, // Open browser on server start
cors: true, // Enable CORS
// Hot Module Replacement
hmr: {
overlay: true // Show error overlay
},
// File watching
watch: {
// Ignore dotfiles
ignored: ['**/.*']
}
}
});
Proxy Configuration for API
export default defineConfig({
server: {
proxy: {
// Proxy API requests to backend
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// WebSocket proxy
'/ws': {
target: 'ws://localhost:3000',
ws: true
},
// Multiple backends
'/v1': {
target: 'http://localhost:3001',
changeOrigin: true
},
'/v2': {
target: 'http://localhost:3002',
changeOrigin: true
}
}
}
});
HTTPS Development Server
import { defineConfig } from 'vite';
import fs from 'node:fs';
export default defineConfig({
server: {
https: {
key: fs.readFileSync('./.cert/key.pem'),
cert: fs.readFileSync('./.cert/cert.pem')
}
}
});
Build Optimization
Code Splitting and Chunking
export default defineConfig({
build: {
rollupOptions: {
output: {
// Manual chunk splitting
manualChunks: {
// Vendor chunks
'react-vendor': ['react', 'react-dom'],
'router-vendor': ['react-router-dom'],
'animation-vendor': ['framer-motion'],
// Feature-based chunks
'dashboard': ['./src/components/views/DashboardView.tsx'],
'reports': ['./src/components/views/ReportsView.tsx']
},
// Asset file naming
assetFileNames: 'assets/[name]-[hash][extname]',
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js'
}
},
// Chunk size warnings
chunkSizeWarningLimit: 500, // KB
// Minification
minify: 'esbuild', // 'terser' | 'esbuild'
// Source maps
sourcemap: true, // or 'inline' | 'hidden'
// Target browsers
target: 'esnext', // or 'es2015', 'es2020', etc.
// CSS code splitting
cssCodeSplit: true
}
});
Advanced Chunking Strategy
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// All node_modules in vendor chunk
if (id.includes('node_modules')) {
// Split large vendors
if (id.includes('framer-motion')) {
return 'vendor-animation';
}
if (id.includes('react') || id.includes('react-dom')) {
return 'vendor-react';
}
return 'vendor';
}
// Component-based splitting
if (id.includes('/components/views/')) {
const viewName = id.split('/components/views/')[1].split('.')[0];
return `view-${viewName.toLowerCase()}`;
}
}
}
}
}
});
Compression and Minification
import { defineConfig } from 'vite';
import { compression } from 'vite-plugin-compression2';
// Install: pnpm add -D vite-plugin-compression2
export default defineConfig({
plugins: [
// Gzip compression
compression({
algorithm: 'gzip',
include: /\.(js|css|html|svg)$/
}),
// Brotli compression
compression({
algorithm: 'brotliCompress',
include: /\.(js|css|html|svg)$/
})
],
build: {
// esbuild is faster, terser produces smaller output
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true
}
}
}
});
CSS and Styling
PostCSS and Tailwind Integration
// vite.config.ts
export default defineConfig({
css: {
postcss: './postcss.config.js',
// CSS modules configuration
modules: {
localsConvention: 'camelCase',
scopeBehaviour: 'local'
},
// Preprocessor options
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
}
});
// postcss.config.js
export default {
plugins: {
'tailwindcss': {},
'autoprefixer': {}
}
};
CSS Code Splitting
export default defineConfig({
build: {
cssCodeSplit: true, // Split CSS per chunk
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
// Organize CSS files
if (assetInfo.name?.endsWith('.css')) {
return 'css/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
}
}
}
}
});
Static Assets
Asset Handling Patterns
// Importing assets (returns URL string) import logo from './assets/logo.png'; import styles from './styles.module.css'; // Explicit URL imports import assetUrl from './asset.png?url'; // Raw content import import rawSvg from './icon.svg?raw'; // Worker import import Worker from './worker?worker'; // JSON import import data from './data.json';
Public Directory
/public
/images
logo.svg
/fonts
custom-font.woff2
favicon.ico
// Public assets are served at root and NOT processed // Reference with absolute path <img src="/images/logo.svg" alt="Logo" /> // ❌ Don't import from public // import logo from '/public/images/logo.svg'; // Wrong! // ✅ Import from src/assets for processing import logo from '@/assets/logo.svg'; // Correct
Asset Configuration
export default defineConfig({
// Public base path
base: '/', // or '/my-app/' for subdirectory hosting
publicDir: 'public', // Default
build: {
assetsDir: 'assets', // Output directory for assets
assetsInlineLimit: 4096, // Inline assets < 4kb as base64
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
// Organize by file type
if (/png|jpe?g|svg|gif|webp|ico/i.test(ext)) {
return 'images/[name]-[hash][extname]';
}
if (/woff2?|ttf|otf|eot/i.test(ext)) {
return 'fonts/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
}
}
}
}
});
Preview Mode
Preview Production Build
export default defineConfig({
preview: {
port: 4173,
strictPort: true,
open: true,
// Proxy config (same as dev server)
proxy: {
'/api': 'http://localhost:3000'
},
// CORS
cors: true,
// Headers
headers: {
'Cache-Control': 'public, max-age=31536000'
}
}
});
Commands:
# Build for production pnpm vite build # Preview production build locally pnpm vite preview # Preview on specific port pnpm vite preview --port 8080
Vite 7 Notes
Vite 7.x introduces:
- •Rolldown - New bundler written in Rust for faster builds (optional)
- •Improved TypeScript support
- •Better tree-shaking
- •Enhanced HMR performance
For demos, the default configuration works well. Advanced bundler options are not typically needed.
Complete Production Config
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { compression } from 'vite-plugin-compression2';
import path from 'node:path';
export default defineConfig(({ mode }) => {
const isDev = mode === 'development';
return {
plugins: [
react(),
// Compression for production (requires vite-plugin-compression2)
!isDev && compression({ algorithm: 'gzip', include: /\.(js|css|html|svg)$/ }),
!isDev && compression({ algorithm: 'brotliCompress', include: /\.(js|css|html|svg)$/ })
].filter(Boolean),
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@utils': path.resolve(__dirname, './src/utils'),
'@types': path.resolve(__dirname, './src/types')
}
},
server: {
port: 5173,
strictPort: true,
open: true,
hmr: {
overlay: true
},
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
sourcemap: !isDev,
minify: isDev ? false : 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'router-vendor': ['react-router-dom'],
'animation-vendor': ['framer-motion']
},
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
if (/png|jpe?g|svg|gif|webp|ico/i.test(ext)) {
return 'images/[name]-[hash][extname]';
}
if (/woff2?|ttf|otf|eot/i.test(ext)) {
return 'fonts/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js'
}
},
chunkSizeWarningLimit: 500
},
preview: {
port: 4173,
strictPort: true,
open: true
}
};
});
Best Practices
- •Use path aliases for clean imports and avoid ../../../ hell
- •Prefix client env vars with VITE_ for automatic exposure
- •Split large vendors into separate chunks for better caching
- •Enable compression for production builds (gzip + brotli)
- •Use .env.local for secrets and never commit to git
- •Configure proxy for API calls to avoid CORS in development
- •Preview builds locally before deploying to catch issues
- •Organize assets by type in build output for better CDN caching
- •Enable sourcemaps in production for debugging (or use 'hidden')
- •Use esbuild for faster builds, terser for smaller output
- •Set base path correctly for subdirectory deployments
- •Test HMR after config changes to ensure Fast Refresh works
Anti-Patterns
- •❌ Forgetting VITE_ prefix on environment variables
- •❌ Importing from /public directory instead of src/assets
- •❌ Committing .env.local with API keys
- •❌ Not configuring path aliases (causes messy imports)
- •❌ Using terser in development (unnecessary slowdown)
- •❌ Disabling CSS code splitting for large apps
- •❌ Not setting strictPort (silent port conflicts)
- •❌ Ignoring chunk size warnings (impacts load time)
- •❌ Missing tsconfig.json paths when using aliases
- •❌ Hardcoding localhost URLs (use env vars)
- •❌ Not testing preview mode before deployment
- •❌ Placing all vendors in single chunk (defeats caching)
- •❌ Configuring proxy for demos (demos are static, no backend)
Feedback Loops
Dev server performance:
# Check HMR speed # Should be < 50ms for most updates # Chrome DevTools → Network → Filter by "vite"
Build analysis:
# Analyze bundle size pnpm vite build --mode production # Output shows chunk sizes # dist/js/vendor-react-abc123.js 142.34 kB # dist/js/index-def456.js 87.21 kB
Preview testing:
# Always preview before deploying pnpm vite build && pnpm vite preview # Test: # - All routes work # - Assets load correctly # - API proxy works (if configured) # - No console errors
Environment validation:
// Add runtime checks for required env vars
if (!import.meta.env.VITE_API_URL) {
throw new Error('VITE_API_URL is required');
}