Configuring Vite manualChunks for Vendor Isolation
1. Error Signature & Symptom Identification
When deploying a Vite 5+ SPA to production, engineers frequently encounter degraded LCP and increased TTFB due to oversized vendor bundles. The browser network waterfall reveals a single vendor file exceeding 1.2 MB, blocking critical rendering paths. This symptom directly conflicts with modern Route-Based Code Splitting & Dynamic Import Strategies where parallel chunk loading is expected.
Error signature
- Single monolithic
vendor-*.jschunk > 1.2 MB - LCP consistently > 2.5 s
- Console warnings regarding long tasks
vite buildoutput generates only onevendor-*.jsdespite multiple third-party dependencies
Root cause
Rollup’s default chunking heuristic in Vite 5 aggressively merges all node_modules dependencies into a single chunk to maximize HTTP/2 multiplexing, ignoring semantic isolation boundaries between UI frameworks and utility libraries.
Diagnostic phase
No configuration changes required. Isolate the baseline before applying overrides:
# Visualize the current chunk composition
npx vite build
# If rollup-plugin-visualizer is configured, open dist/stats.html
# Otherwise inspect the dist/assets/ directory:
du -sh dist/assets/*.js | sort -hrConfirm a single vendor-*.js file exceeds 1.2 MB and contains more than 15 distinct third-party packages.
2. Root Cause Analysis & Rollup Chunking Heuristics
Vite delegates chunk generation to Rollup. Without an explicit manualChunks function, all node_modules imports that appear in multiple entry points or dynamic chunks are automatically coalesced into a single shared chunk. Understanding Vendor Chunk Isolation and Third-Party Management is critical for overriding this behavior without fragmenting the dependency graph.
Analysis phase
npx vite build --debugInspect the Rollup chunk graph output in the terminal logs to verify which modules are grouped into commonjsHelpers or the shared vendor entry. Look for lines like Generated chunks and Chunk sizes.
3. Exact Configuration & CLI Implementation
Override the default chunking strategy in vite.config.ts using a deterministic manualChunks function that maps specific dependency trees to isolated cache groups while preserving tree-shaking integrity.
Configuration patch
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('/react/') || id.includes('/react-dom/') || id.includes('/scheduler/')) {
return 'react-vendor';
}
if (id.includes('/lodash') || id.includes('/date-fns')) {
return 'utils-vendor';
}
if (id.includes('/axios/') || id.includes('/graphql/')) {
return 'network-vendor';
}
return 'vendor'; // Catch-all for remaining node_modules
}
}
}
}
}
});Verification
npx vite build
ls -lh dist/assets/*.jsVerify the dist/assets/ directory contains react-vendor-*.js, utils-vendor-*.js, and network-vendor-*.js, each below 400 KB.
4. Debugging Workflow & Edge-Case Resolution
Post-configuration, circular dependencies between isolated vendor chunks may trigger duplicate module inclusion or hydration mismatches in SSR contexts.
Error signature
- Duplicate module warnings in browser console:
X was imported twice with different chunks ReferenceErrorduring hydration- Unexpected chunk size regression (a sub-dependency is now duplicated in two chunks)
Root cause
Over-segmentation causes shared sub-dependencies (e.g., tslib, @babel/runtime) to be included in multiple vendor chunks because the manualChunks function returns different group names for modules that import the same helper.
Fallback logic patch
Add a shared runtime group at the highest priority so that tiny shared helpers always land in one deterministic chunk:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// Shared runtime helpers — must resolve to a single chunk
const sharedRuntime = ['tslib', '@babel/runtime', 'core-js'];
if (sharedRuntime.some(dep => id.includes(`/node_modules/${dep}/`))) {
return 'shared-runtime';
}
if (id.includes('/react/') || id.includes('/react-dom/') || id.includes('/scheduler/')) {
return 'react-vendor';
}
if (id.includes('/lodash') || id.includes('/date-fns')) {
return 'utils-vendor';
}
if (id.includes('/axios/') || id.includes('/graphql/')) {
return 'network-vendor';
}
return 'vendor';
}
}
}
}
}
});Verification
npx vite buildConfirm zero duplicate module warnings in the Rollup output. Verify shared-runtime-*.js is below 50 KB and is referenced exactly once in the network tab (loaded via <link rel="modulepreload">).
5. Verification Metric & Production Rollout
Final validation requires quantitative bundle analysis and real-user monitoring correlation. Ensure chunk isolation aligns with HTTP/2 parallelism limits and CDN caching headers.
Rollout CLI chain
# Build and inspect the output
npx vite build
# Analyze each chunk's module composition
npx source-map-explorer dist/assets/*.js
# Serve locally with correct cache headers for manual verification
npx http-server dist -p 8080 --cors --cache-control "public, max-age=31536000, immutable"Success metrics
- LCP improvement ≥ 30% versus the monolithic baseline
- Vendor chunk count stabilized at 3–5
- Each individual vendor chunk ≤ 350 KB
- Vendor chunks served with
Cache-Control: immutableachieve high cache hit rates on repeat visits - Zero
Uncaught SyntaxErroror duplicate module warnings in production logs
Note: vite build does not accept a --reporter json flag. To get structured build output for CI, use rollup-plugin-visualizer configured in vite.config.ts with json: true in its options, which writes a machine-readable stats.json alongside the HTML report.