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.ts with TypeScript types
-
Install React plugin: pnpm add -D @vitejs/plugin-react
-
Configure path aliases for clean imports
-
Set up environment variables with .env files
-
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'); }