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-*.js chunk > 1.2 MB
  • LCP consistently > 2.5 s
  • Console warnings regarding long tasks
  • vite build output generates only one vendor-*.js despite 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 -hr

Confirm 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 --debug

Inspect 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/*.js

Verify 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
  • ReferenceError during 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 build

Confirm 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: immutable achieve high cache hit rates on repeat visits
  • Zero Uncaught SyntaxError or 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.