Fix: Webpack Bundle Too Large — Chunk Size Warning
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.jsOr 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 MiBWhy 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-uiadded 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-envtargets 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 bundleThe 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.jsonFix 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-reactchunk 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 duplicateUse 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Webpack Bundle Size Too Large — Reduce JavaScript Bundle for Faster Load Times
How to reduce Webpack bundle size — code splitting, tree shaking, dynamic imports, bundle analysis, moment.js replacement, lodash optimization, and production build configuration.
Fix: WebAssembly (WASM) Not Working — Module Fails to Load, Memory Error, or JS Interop Broken
How to fix WebAssembly issues — instantiateStreaming vs instantiate, CORS for WASM files, linear memory limits, wasm-bindgen JS interop, imports/exports mismatch, and WASM in bundlers.
Fix: React useTransition Not Working — UI Still Freezes, isPending Never True, or Transition Not Deferred
How to fix React useTransition and startTransition issues — what counts as a transition, Suspense integration, concurrent rendering requirements, and common mistakes that prevent transitions from deferring.
Fix: Web Worker Not Working — postMessage Ignored, Cannot Import Module, or Worker Crashes Silently
How to fix Web Worker issues — postMessage data cloning, module workers, error handling, SharedArrayBuffer setup, Comlink, and common reasons workers silently fail.