Fix: Vite Build Chunk Size Warning (Some Chunks Are Larger Than 500 kB)
Quick Answer
How to fix Vite's chunk size warning — why bundles exceed 500 kB, how to split code with dynamic imports and manualChunks, configure the chunk size limit, and optimize your Vite production build.
The Error
Running vite build completes but shows a warning:
vite v5.0.0 building for production...
✓ 1234 modules transformed.
dist/assets/index-Bh7kRCDa.js 1,823.45 kB │ gzip: 521.30 kB
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimitThe build succeeds, but the large bundle means slow initial page loads, especially on mobile connections.
Why This Happens
Vite uses Rollup for production builds. By default, Rollup bundles all imported modules into as few chunks as possible. The 500 kB warning fires when a single output file exceeds that size after minification (before gzip).
Common causes:
- Large third-party libraries bundled into the main chunk — lodash, moment.js, chart libraries, UI component libraries included all at once.
- No code splitting — every route’s code is bundled into a single file instead of being loaded on demand.
- Importing entire libraries instead of specific functions —
import _ from 'lodash'pulls in all of lodash;import { debounce } from 'lodash'should tree-shake the rest, but not all libraries support tree-shaking. - Multiple large dependencies — even if individually small, several 100 kB libraries add up.
Fix 1: Use Dynamic Imports for Route-Level Code Splitting
The most impactful change — load route components only when the user navigates to them:
React with React Router:
// Before — all routes bundled into one chunk
import HomePage from './pages/HomePage';
import DashboardPage from './pages/DashboardPage';
import SettingsPage from './pages/SettingsPage';
import ReportsPage from './pages/ReportsPage';
// After — each route is a separate chunk loaded on demand
import { lazy, Suspense } from 'react';
const HomePage = lazy(() => import('./pages/HomePage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
const ReportsPage = lazy(() => import('./pages/ReportsPage'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/reports" element={<ReportsPage />} />
</Routes>
</Suspense>
);
}Vue with Vue Router:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('./pages/HomePage.vue'), // Lazy loaded
},
{
path: '/dashboard',
component: () => import('./pages/DashboardPage.vue'), // Lazy loaded
},
{
path: '/settings',
component: () => import('./pages/SettingsPage.vue'), // Lazy loaded
},
],
});After adding dynamic imports, run vite build again — each route becomes a separate .js chunk.
Fix 2: Split Vendor Chunks with manualChunks
For third-party libraries that are used across multiple routes, split them into separate vendor chunks so they can be cached independently:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
// Group large vendor libraries into named chunks
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-ui': ['@mui/material', '@emotion/react', '@emotion/styled'],
'vendor-charts': ['recharts', 'd3'],
'vendor-utils': ['lodash', 'date-fns', 'axios'],
},
},
},
},
});Dynamic manualChunks function (more flexible — auto-groups all node_modules):
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// Put each package in its own chunk
const packageName = id
.toString()
.split('node_modules/')[1]
.split('/')[0]
.replace('@', '');
return `vendor-${packageName}`;
}
},
},
},
},
});Warning: Creating too many small chunks adds HTTP request overhead. Aim for a balance — group related libraries together. More than 20–30 chunks is usually counterproductive unless you are using HTTP/2.
Fix 3: Replace Heavy Libraries with Lighter Alternatives
Some libraries are inherently large. Replace them with lighter alternatives:
moment.js (329 kB) → date-fns or dayjs:
npm uninstall moment
npm install date-fns # Tree-shakeable — only imports what you use
# or
npm install dayjs # 2 kB, moment-compatible API// Before
import moment from 'moment';
const formatted = moment(date).format('YYYY-MM-DD');
// After (date-fns — tree-shakeable)
import { format } from 'date-fns';
const formatted = format(date, 'yyyy-MM-dd');
// After (dayjs)
import dayjs from 'dayjs';
const formatted = dayjs(date).format('YYYY-MM-DD');lodash (72 kB) — import specific functions:
// Before — imports all of lodash
import _ from 'lodash';
const result = _.debounce(fn, 300);
// After — imports only debounce (~2 kB)
import debounce from 'lodash/debounce';
// or
import { debounce } from 'lodash-es'; // ES module version — tree-shakeableReplace heavy charting libraries with lighter ones:
| Heavy | Lighter alternative |
|---|---|
| Chart.js (200 kB) | uPlot (40 kB) for line charts |
| Highcharts (400 kB) | Recharts (300 kB) or Victory |
| Three.js (600 kB) | Load dynamically only on pages that use it |
Fix 4: Analyze Bundle Contents
Before optimizing, identify what is actually inside the large chunk:
Use rollup-plugin-visualizer:
npm install --save-dev rollup-plugin-visualizer// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({
filename: 'dist/stats.html',
open: true, // Opens the report in browser after build
gzipSize: true, // Shows gzip sizes
brotliSize: true,
}),
],
});Run vite build — a visual treemap opens in your browser showing exactly which modules are largest.
Use vite-bundle-analyzer:
npx vite-bundle-analyzerIdentify the top contributors and focus optimization effort there.
Fix 5: Enable Build Optimizations in vite.config.js
Additional Vite build options to reduce bundle size:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
// Increase the warning threshold if the warning is acceptable
chunkSizeWarningLimit: 1000, // kB — suppresses warning for chunks under 1 MB
// Enable minification (enabled by default with esbuild)
minify: 'esbuild', // Fast, or 'terser' for better compression
// Enable CSS code splitting
cssCodeSplit: true,
// Generate source maps only for debugging (omit in prod to save space)
sourcemap: false,
rollupOptions: {
output: {
// Limit chunk file count to improve caching
experimentalMinChunkSize: 20_000, // Merge chunks smaller than 20 kB
},
},
},
});Suppress the warning without fixing it (only appropriate if you’ve analyzed the bundle and the size is acceptable):
build: {
chunkSizeWarningLimit: 1500, // Raise threshold to 1500 kB
},Fix 6: Lazy Load Heavy Components
Individual heavy components (rich text editors, code editors, maps, PDF viewers) should be loaded on demand:
// React — lazy load Monaco Editor (large)
import { lazy, Suspense, useState } from 'react';
const MonacoEditor = lazy(() => import('@monaco-editor/react'));
function CodeEditorPage() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<button onClick={() => setShowEditor(true)}>Open Editor</button>
{showEditor && (
<Suspense fallback={<div>Loading editor...</div>}>
<MonacoEditor language="javascript" value="// code here" />
</Suspense>
)}
</div>
);
}Vue — lazy load on intersection (load when visible):
// components/HeavyChart.vue is only loaded when it enters the viewport
const HeavyChart = defineAsyncComponent(() => import('./components/HeavyChart.vue'));Still Not Working?
Check tree-shaking is working. Some libraries are not tree-shakeable because they use CommonJS exports. Check if an ES module version is available:
# Check if a package has an ES module entry point
cat node_modules/some-library/package.json | grep '"module"\|"exports"'If the library does not have an ESM entry, it cannot be tree-shaken. Look for an alternative or import only what you need manually.
Check for barrel files in your own code. A barrel file (index.ts that re-exports everything) defeats tree-shaking:
// src/components/index.ts — barrel file
export { Button } from './Button';
export { Modal } from './Modal';
export { DataTable } from './DataTable'; // Heavy component
// Importing Button also pulls in DataTable if bundler cannot tree-shake
import { Button } from '../components';
// Fix — import directly
import { Button } from '../components/Button';Verify your Vite version supports the optimization you need. Vite 5+ significantly improved code splitting. If you are on Vite 3 or 4, upgrade:
npm install vite@latestFor related build and bundling issues, see Fix: Vite Failed to Resolve Import and Fix: webpack Module Not Found.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: CSS Custom Properties (Variables) Not Working or Not Updating
How to fix CSS custom properties not applying — wrong scope, missing fallback values, JavaScript not setting variables on the right element, and how CSS variables interact with media queries and Shadow DOM.
Fix: Next.js Middleware Not Running (middleware.ts Not Intercepting Requests)
How to fix Next.js middleware not executing — wrong file location, matcher config errors, middleware not intercepting API routes, and how to debug middleware execution in Next.js 13 and 14.
Fix: Next.js Build Failed (next build Errors and How to Fix Them)
How to fix Next.js build failures — TypeScript errors blocking production builds, module resolution failures, missing environment variables, static generation errors, and common next build crash causes.
Fix: Vue 3 Reactive Data Not Updating (ref/reactive Not Triggering Re-render)
How to fix Vue 3 reactive data not updating the UI — why ref and reactive lose reactivity, how to correctly mutate reactive state, and common pitfalls with destructuring and nested objects.