Why Vite Build Performance Matters
Vite's development server is famously fast, but production builds tell a different story. A poorly configured Vite project can produce multi-megabyte bundles that tank your Lighthouse scores and drive users away before your app even loads. In 2026, users abandon pages that take more than 3 seconds to load, and Google's Core Web Vitals directly impact search ranking.
This guide dives deep into Vite's production build pipeline and shows you the specific configurations that move the needle from a 60-point Lighthouse score to 95+.
Understanding Vite's Build Pipeline
Vite uses Rollup under the hood for production builds. Every optimization technique available in Rollup is available in Vite — configured through build.rollupOptions. Vite's dev mode uses esbuild for near-instant transforms; prod mode uses Rollup for deep optimizations. They are fundamentally different pipelines.
Code Splitting Strategies
Manual Chunks — The Highest-Impact Change
The default Vite strategy puts all node_modules content into a single vendor chunk. For a medium-sized app this creates a 500KB+ chunk that must download and parse before anything renders.
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// React runtime — changes rarely, cache forever
'react-vendor': ['react', 'react-dom'],
// UI component library — heavy but stable
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
// Routing
'router': ['react-router-dom'],
// Utility libraries
'utils': ['lodash-es', 'date-fns', 'zod'],
},
},
},
},
})
When you ship separate chunks, a returning user after a minor app update only re-downloads chunks that actually changed. The react-vendor chunk never changes and is served from browser cache instantly.
Function-Based Splitting for Larger Apps
function manualChunks(id: string) {
if (id.includes('/src/pages/')) {
const match = id.match(/\/pages\/([^/]+)/)
if (match) return `page-${match[1]}`
}
if (id.includes('node_modules')) {
if (id.includes('recharts') || id.includes('d3')) return 'charts-vendor'
if (id.includes('@codemirror') || id.includes('monaco-editor')) return 'editor-vendor'
if (id.includes('framer-motion')) return 'animation-vendor'
return 'vendor'
}
}
export default defineConfig({
build: { rollupOptions: { output: { manualChunks } } },
})
Route-Based Code Splitting with React.lazy
// App.tsx
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const Analytics = lazy(() => import('./pages/Analytics'))
export function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
)
}
Users who never visit /analytics never download 280KB of charting code.
Tree Shaking: Making It Actually Work
Tree shaking eliminates dead code, but only when dependencies use ES modules with side-effect-free exports.
The lodash Problem and Solution
// Bad: imports ENTIRE lodash (~70KB gzipped)
import _ from 'lodash'
const result = _.groupBy(items, 'category')
// Good: lodash-es supports tree shaking
import { groupBy } from 'lodash-es'
const result = groupBy(items, 'category')
Bundle Analysis with rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
filename: 'dist/bundle-analysis.html',
gzipSize: true,
brotliSize: true,
open: true, // Auto-open after build
}),
],
})
Run vite build and the visualizer opens automatically showing an interactive treemap of your bundle. Every rectangle is a module; size equals bytes. This is how you discover moment.js with full locale data hiding in your date picker.
Rollup Advanced Configuration
Minification and esbuild Options
export default defineConfig({
build: {
// esbuild is the default — fast and well-integrated
minify: 'esbuild',
// Drop console/debugger statements in production
esbuildOptions: {
drop: ['console', 'debugger'],
},
// Target modern browsers — less transpilation = smaller output
target: 'es2020',
},
})
Asset Configuration
export default defineConfig({
build: {
assetsInlineLimit: 4096, // Inline assets < 4KB as base64
cssCodeSplit: true, // Separate CSS per chunk for parallel loading
sourcemap: 'hidden', // Source maps for error monitoring, not visible to users
rollupOptions: {
output: {
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
},
})
Build Performance: Faster CI Pipelines
export default defineConfig({
optimizeDeps: {
include: ['react', 'react-dom', 'react-router-dom'],
exclude: ['@vite/client', '@vite/env'],
},
build: {
rollupOptions: {
maxParallelFileOps: 20,
},
},
})
Environment-Specific Configuration
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
define: {
__APP_VERSION__: JSON.stringify(env.npm_package_version),
__ENABLE_ANALYTICS__: mode === 'production',
},
build: {
sourcemap: mode === 'staging' ? true : 'hidden',
chunkSizeWarningLimit: 500, // KB — warn on large chunks
},
}
})
Measuring Your Results
# Build and analyze
vite build --mode production
# Serve and run Lighthouse
npm install -g serve
serve dist &
npx lighthouse http://localhost:3000 --view
Key Lighthouse metrics to track:
- FCP (First Contentful Paint): Target < 1.8s
- LCP (Largest Contentful Paint): Target < 2.5s
- TBT (Total Blocking Time): Target < 200ms
- CLS (Cumulative Layout Shift): Target < 0.1
Real-World Results: Dashboard App Case Study
| Metric | Before | After | Change |
|---|---|---|---|
| Bundle size (gzip) | 847 KB | 312 KB | −63% |
| Chunk count | 1 | 8 | +7 |
| LCP | 4.2s | 1.8s | −57% |
| TBT | 680ms | 120ms | −82% |
| Lighthouse score | 61 | 94 | +33 pts |
The biggest single win: moving recharts and d3 to their own chunk and lazy-loading the analytics page.
Summary
Vite build optimization is about understanding how browsers load code. Split vendor dependencies by stability, use dynamic imports for routes users might never visit, verify tree shaking with the visualizer plugin, and measure with Lighthouse after every significant change. Ten minutes of configuration gets you from "adequate" to "fast."