Skip to content

Fix: Vite Build Chunk Size Warning (Some Chunks Are Larger Than 500 kB)

FixDevs ·

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.chunkSizeWarningLimit

The 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 functionsimport _ 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-shakeable

Replace heavy charting libraries with lighter ones:

HeavyLighter 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-analyzer

Identify 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@latest

For related build and bundling issues, see Fix: Vite Failed to Resolve Import and Fix: webpack Module Not Found.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles