Practical code splitting with Webpack 5
Reduce initial bundle size and improve Time-to-Interactive using dynamic imports and SplitChunksPlugin.
21 Aug 2025, 18:41 UTC

Problem: initial bundle too large
When a JavaScript application grows beyond a few hundred lines, the bundled script shipped to the browser can easily exceed 200 KB (minified). The browser must parse, compile, and execute this code before any UI appears, inflating Time‑to‑Interactive (TTI) and making the page feel sluggish on mobile networks.
Useful takeaway
Enable code splitting with dynamic import() and SplitChunksPlugin so the browser only downloads the code needed for the current view. Keeping the initial chunk under ~80 KB typically reduces TTI by 30‑50 % in single‑page applications.
Requirements for a minimal split‑enabled build
- Node ≥ 14 (Webpack 5 expects a recent Node version)
- Webpack 5 (or Webpack 4 with comparable plugins) installed via
npm i -D webpack webpack-cli - A
package.jsonscript that runswebpack --mode production - Optional:
webpack-bundle-analyzerfor visual confirmation (not required for the split itself)
Smallest suitable design
- Entry point – Define a single entry (
app) that loads the core UI. - Dynamic import – Replace direct
importstatements for non‑critical code withimport(). Webpack automatically creates a separate chunk for eachimport()call. - SplitChunksPlugin – Configure
optimization.splitChunksto share common modules (e.g., React, lodash) across chunks instead of duplicating them.
Example webpack configuration (production mode)
{
"mode": "production",
"entry": {
"app": "./src/index.js"
},
"optimization": {
"splitChunks": {
"chunks": "all",
"minSize": 20000, // 20 KB before split is considered
"minRemainingSize": 0
},
"runtimeChunk": "single"
},
"module": {
"rules": [
{
"test": /\.js$/,
"use": "babel-loader",
"exclude": /node_modules/
}
]
}
}
A typical lazy‑loaded component
// src/components/Analytics.js – loaded only when the user visits the reporting page
import(/* webpackChunkName: "analytics" */ "./analytics.js")
.then(module => {
// use module.default or module.exports as needed
console.log(module.generateReport());
})
.catch(err => {
console.error("Failed to load analytics:", err);
});
Running the build and inspecting the result
Command location: project root, where package.json lives.
Required permissions: the user must have read access to node_modules and write access to the output directory (default dist/). No sudo needed if node_modules is already installed.
Command: webpack --profile --json > stats.json
Expected checks: open stats.json and look for an array named chunks. Each chunk object contains name, files, and modules. Verify that the app chunk lists only the core modules, while the analytics chunk appears as a separate entry.
Risk: Running the command without a proper webpack.config.js may cause Webpack to fall back to its default configuration, potentially bundling everything into a single file and null‑splitting the effort.
Verifying the split in the browser
- Start a local development server:
npx webpack serveornpm run dev. - Open the page in Chrome or Firefox.
- Open DevTools → Network tab.
- Filter by JS name or
document_type. - Observe that the initial request contains only the
appbundle; subsequent interactions (e.g., clicking a “Show analytics” button) trigger a second request namedanalytics.js(or whatever chunk name you configured). - Use the Performance panel to confirm that TTI improves when the lazy load is exercised.
Limitations and when the design may need to change
- Too many chunks: If every feature is lazy‑loaded, the page may issue 10‑15 separate HTTP requests, increasing latency on high‑round‑trip networks. Profile with Lighthouse; aim for 2‑3 critical chunks and group related lazy modules under the same chunk name when they are used together.
- Legacy browsers:
import()is not natively supported in Internet Explorer. Add a core-js or@babel/preserve-modulespolyfill, or transpile the import syntax with Babel before Webpack processes it. - Module duplication: Without
SplitChunksPlugin, common dependencies may be inlined in every chunk, inflating total bytes. The defaultsplitChunksconfiguration already addresses this, but customenforceSizeThresholdvalues can unintentionally keep large modules together. - Build‑time overhead: Aggressive splitting can increase the time Webpack spends analyzing the module graph. For very large codebases (> 200 k modules) consider incremental builds or the
profileflag only during optimization passes.
Practical way to check the result after deployment
Real‑User Monitoring (RUM): Tools such as Google Analytics 4’s “web vitals” or Speedtracker capture TTI per visitor. Compare the median TTI before and after enabling code splitting; a reduction of ≥ 150 ms is a typical healthy sign.
Bundle‑size budget: Add a CI step that fails if any chunk exceeds a target size (e.g., maxSize: 80000 bytes). webpack-bundle-analyzer can generate a report that CI can parse.
Failure modes that would force a redesign
- If the application’s core UI depends on a module that is split into a lazy‑loaded chunk and that chunk fails to load (network error, CORS block), the UI may throw an uncaught error. Mitigate by ensuring critical code stays in the initial bundle or by showing a graceful fallback.
- When
SplitChunksPluginis configured withminSizeset too low (e.g., 1 KB), Webpack may create dozens of micro‑chunks, defeating the purpose. Re‑evaluate the threshold after an initial profiling run. - Introducing module federation on top of code splitting can shift the runtime loading responsibility to a host application. Verify that remote entries are pre‑loaded or pre‑bootstrapped according to your architecture roadmap.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.