React Vite: Fast Build Tool and Dev Server Setup (2026)
Vite has become the default build tool for new React projects — replacing Create React App with dramatically faster cold starts, near-instant HMR and a streamlined plugin ecosystem. By leveraging native ES modules in development and Rollup for production bundles, Vite achieves millisecond feedback loops even in large codebases.
Table of Contents
Project Setup
# New React + TypeScript project
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
# Available templates
# react, react-ts, react-swc, react-swc-ts
Vite dev server starts in under 300ms regardless of project size because it doesn't bundle — it serves ES modules directly to the browser and only transforms files on demand.
vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc' // SWC for faster transforms
import path from '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'),
},
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
},
},
},
},
})
Environment Variables
Vite uses VITE_ prefix for variables exposed to the client:
# .env — loaded always
# .env.local — loaded always, gitignored
# .env.development — loaded in dev mode
# .env.production — loaded in production build
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
# Server-only (no VITE_ prefix — NOT exposed to browser)
DATABASE_URL=postgresql://...
SECRET_KEY=...
// Access in React code
const apiUrl = import.meta.env.VITE_API_URL
const appTitle = import.meta.env.VITE_APP_TITLE
const isDev = import.meta.env.DEV
const isProd = import.meta.env.PROD
const mode = import.meta.env.MODE // 'development' | 'production'
// TypeScript — extend ImportMetaEnv
// src/vite-env.d.ts
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Path Aliases
// vite.config.ts — already shown above
// tsconfig.json — keep TypeScript in sync with Vite aliases
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"]
}
}
}
// Now use clean imports everywhere
import { Button } from '@/components/ui/Button'
import { useAuth } from '@/hooks/useAuth'
import { formatDate } from '@/utils/date'
Dev Server Proxy
// vite.config.ts
server: {
proxy: {
// Simple proxy
'/api': 'http://localhost:8080',
// With options
'/auth': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
},
// WebSocket proxy
'/ws': {
target: 'ws://localhost:8080',
ws: true,
},
// Rewrite path
'/old-api': {
target: 'http://localhost:8080',
rewrite: (path) => path.replace(/^\/old-api/, '/new-api'),
},
},
}
Key Plugins
npm install -D @vitejs/plugin-react-swc # React with SWC (faster than Babel)
npm install -D vite-plugin-svgr # Import SVGs as React components
npm install -D vite-tsconfig-paths # Auto-sync tsconfig paths
npm install -D rollup-plugin-visualizer # Bundle size visualization
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import svgr from 'vite-plugin-svgr'
import tsconfigPaths from 'vite-tsconfig-paths'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
react(),
svgr(), // import Logo from './logo.svg?react'
tsconfigPaths(), // auto-reads paths from tsconfig.json
visualizer({
open: true,
filename: 'dist/stats.html',
}),
],
})
Build Optimization
// Manual chunks — control vendor splitting
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'react-vendor'
if (id.includes('@tanstack')) return 'tanstack'
if (id.includes('lucide')) return 'icons'
return 'vendor'
}
},
},
},
// Chunk size warning threshold
chunkSizeWarningLimit: 600,
// Minify with esbuild (default) or terser
minify: 'esbuild',
}
// Dynamic imports — Vite automatically splits at import() boundaries
const HeavyComponent = lazy(() => import('./HeavyComponent'))
// Preload hints — Vite injects modulepreload links automatically
Testing with Vitest
Vitest is Vite-native and shares the same config:
npm install -D vitest @testing-library/react @testing-library/user-event jsdom
// vite.config.ts — add test config
/// <reference types="vitest" />
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
css: true,
},
})
// src/test/setup.ts
import '@testing-library/jest-dom'
// Writing tests — same API as Jest
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
test('increments count on click', async () => {
render(<Counter />)
await userEvent.click(screen.getByRole('button', { name: /increment/i }))
expect(screen.getByText('1')).toBeInTheDocument()
})
Migrating from CRA
- Install Vite:
npm install -D vite @vitejs/plugin-react-swc - Create
vite.config.tswith React plugin - Move
public/index.htmlto root and add<script type="module" src="/src/main.tsx"></script> - Rename
REACT_APP_*env vars toVITE_* - Replace
process.env.REACT_APP_*withimport.meta.env.VITE_* - Update
package.jsonscripts:"dev": "vite","build": "vite build","preview": "vite preview" - Remove
react-scriptsdependency
Performance comparison: A typical CRA project cold-starts in 15–30 seconds. The same project with Vite starts in under 500ms. HMR updates that took 3–5 seconds in CRA happen in under 50ms with Vite.