Skip to content

Fix: Webpack Bundle Too Large — Chunk Size Warning

FixDevs ·

Quick Answer

How to reduce Webpack bundle size — code splitting, lazy loading, tree shaking, analyzing the bundle with webpack-bundle-analyzer, replacing heavy dependencies, and configuring splitChunks.

The Error

Webpack warns about oversized bundles during build:

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  main.js (1.23 MiB)

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). The limit can be adjusted using "performance.maxEntrypointSize" in webpack config.
Entrypoints:
  main (1.23 MiB)
      main.js

Or Core Web Vitals suffer because users download a huge JavaScript file before the page loads.

The page loads slowly and Lighthouse shows:

Reduce unused JavaScript — Potential savings of 800 KiB
Avoid enormous network payloads — Total size was 2.1 MiB

Why This Happens

A large bundle means users download code they may not need:

  • No code splitting — all routes and components bundled into a single file. Users downloading the login page also download the dashboard, admin panel, and all other routes.
  • Heavy dependencies — libraries like moment.js (67 KB gzipped), lodash (24 KB gzipped), or @material-ui added without care for what’s actually used.
  • No tree shaking — unused exports from libraries are included because imports aren’t written in a tree-shakeable way.
  • Large assets bundled as JavaScript — images, SVGs, or JSON data embedded as base64 or JS objects.
  • Missing production optimization — bundle built without mode: 'production', so minification and dead code elimination don’t run.
  • All polyfills included@babel/preset-env targets too broad a browser range, adding polyfills for features modern browsers support natively.

Fix 1: Analyze the Bundle First

Before optimizing, identify what’s actually large with webpack-bundle-analyzer:

npm install -D webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',      // Opens an HTML report
      reportFilename: 'bundle-report.html',
      openAnalyzer: false,
    }),
  ],
};
npm run build
# Opens bundle-report.html — treemap showing what's in your bundle

The treemap shows each module’s size. Common culprits to look for: entire moment.js locale files, all of lodash, large icon libraries, duplicate packages at different versions.

Quick analysis without the plugin:

npx webpack --json=stats.json
npx webpack-bundle-analyzer stats.json

Fix 2: Enable Code Splitting with Dynamic Imports

Split the bundle by route or feature so users only download what they need:

// BEFORE — everything in one bundle
import { Dashboard } from './pages/Dashboard';
import { Settings } from './pages/Settings';
import { AdminPanel } from './pages/AdminPanel';

// AFTER — each page loaded on demand
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
const AdminPanel = React.lazy(() => import('./pages/AdminPanel'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/admin" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  );
}

Webpack automatically creates separate chunks for each dynamic import. The Dashboard chunk is only downloaded when the user navigates to /dashboard.

Split heavy libraries:

// Load a heavy library only when needed
async function exportToPDF() {
  const { jsPDF } = await import('jspdf');
  const doc = new jsPDF();
  // ...
}

Fix 3: Configure splitChunks for Optimal Caching

Split vendor libraries into separate chunks so they’re cached by the browser independently from your app code:

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // Split React and ReactDOM into their own chunk
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
          name: 'vendor-react',
          chunks: 'all',
          priority: 20,
        },
        // Group other large vendors
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
          minSize: 50000,  // Only split if > 50KB
        },
      },
    },
    // Extract the runtime into its own tiny chunk
    runtimeChunk: 'single',
  },
};

Benefits:

  • vendor-react chunk rarely changes → long cache lifetime
  • Your app code changes frequently → shorter cache
  • Users only re-download what actually changed on deploy

Fix 4: Replace Heavy Dependencies

The biggest wins often come from swapping heavy libraries for lighter alternatives:

Moment.js → Day.js or date-fns:

# Moment.js: 67 KB gzipped
# Day.js: 2 KB gzipped (same API)
npm install dayjs
npm uninstall moment
// Before
import moment from 'moment';
const date = moment('2026-03-20').format('MMMM D, YYYY');

// After — Day.js (same API, 33x smaller)
import dayjs from 'dayjs';
const date = dayjs('2026-03-20').format('MMMM D, YYYY');

lodash → native methods or lodash-es:

// BEFORE — imports all of lodash (71 KB gzipped)
import _ from 'lodash';
const unique = _.uniq(arr);
const grouped = _.groupBy(items, 'category');

// AFTER — import only what you use (tree-shakeable)
import uniq from 'lodash/uniq';
import groupBy from 'lodash/groupBy';

// OR — use native methods for simple operations
const unique = [...new Set(arr)];

Or use babel-plugin-lodash to automatically cherry-pick:

npm install -D babel-plugin-lodash lodash-webpack-plugin
// babel.config.js
module.exports = {
  plugins: ['lodash'],
};

// webpack.config.js
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
module.exports = {
  plugins: [new LodashModuleReplacementPlugin()],
};

Icon libraries — import only what you use:

// BEFORE — imports all of Material Icons (1.5 MB)
import { Delete, Edit, Add } from '@mui/icons-material';

// AFTER — import each icon individually (tree-shakeable)
import Delete from '@mui/icons-material/Delete';
import Edit from '@mui/icons-material/Edit';
import Add from '@mui/icons-material/Add';

Fix 5: Tree Shake Unused Code

Webpack tree shakes ES module exports automatically in production mode, but only if imports are written correctly:

// TREE-SHAKEABLE — named imports
import { formatDate, parseDate } from './utils';

// NOT TREE-SHAKEABLE — imports the whole module object
import * as utils from './utils';
utils.formatDate(date);

// NOT TREE-SHAKEABLE — CommonJS require
const { formatDate } = require('./utils');

Enable production mode — tree shaking only runs in production:

// webpack.config.js
module.exports = {
  mode: 'production',  // Enables tree shaking, minification, dead code elimination
};

Mark packages as side-effect-free — tells Webpack it’s safe to remove unused exports:

// package.json (in your own library or app)
{
  "sideEffects": false
}

// Or specify files that DO have side effects
{
  "sideEffects": ["*.css", "src/polyfills.js"]
}

Fix 6: Lazy Load Images and Other Assets

Images embedded as base64 in JavaScript dramatically increase bundle size:

// webpack.config.js — inline only tiny images, use file-loader for large ones
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024,  // Inline only if < 4KB; otherwise use separate file
          },
        },
      },
    ],
  },
};

For large SVG icon sets, use a sprite or on-demand loading:

// Instead of importing all SVGs upfront
import { ReactComponent as Logo } from './logo.svg';

// For large sets, lazy load:
const Icon = React.lazy(() => import(`./icons/${iconName}.svg`));

Fix 7: Optimize Babel Targets

@babel/preset-env with broad targets includes polyfills for old browsers that most users no longer use. Target only what you actually support:

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      // Before: "last 2 versions" — includes IE 11 polyfills
      // After: modern browsers only
      targets: '> 0.5%, last 2 versions, not dead, not IE 11',

      // Only include polyfills for features actually used
      useBuiltIns: 'usage',
      corejs: 3,
    }],
  ],
};

Or use browserslist in package.json:

{
  "browserslist": [
    "> 0.5%",
    "last 2 versions",
    "not dead",
    "not IE 11"
  ]
}

Removing IE 11 support alone can cut 50-100 KB from the bundle by eliminating polyfills for Promise, fetch, Symbol, and dozens of other features.

Still Not Working?

Check for duplicate packages. If two packages depend on different versions of the same library, both versions get bundled:

npm dedupe               # Flatten dependency tree
npx webpack --stats-all | grep duplicate

Use externals for CDN-loaded libraries. If React is already loaded via a CDN, tell Webpack not to bundle it:

// webpack.config.js
module.exports = {
  externals: {
    react: 'React',       // Use window.React from CDN
    'react-dom': 'ReactDOM',
  },
};

Compress with Brotli or gzip. The actual download size depends on compression, which Webpack doesn’t control — configure your server:

# nginx.conf
gzip on;
gzip_types text/javascript application/javascript;
brotli on;
brotli_types text/javascript application/javascript;

A 500 KB JS file compresses to ~150 KB with gzip and ~120 KB with Brotli. Compression is often more impactful than bundle optimization for initial load time.

For related issues, see Fix: Vite Build Chunk Size Warning 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