Finding and killing bundle bloat with webpack-bundle-analyzer
A practical guide to reading the treemap, the four bloat patterns we find in almost every audit, and the exact config changes that fixed each one.
webpack-bundle-analyzer turns your production bundle into an interactive treemap - box size proportional to gzipped bytes - and it's the single fastest way to find out why a bundle is bigger than it should be. Wiring it up takes two lines. Reading the result usefully takes knowing what you're looking for.
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
process.env.ANALYZE && new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false }),
].filter(Boolean),
};Pattern 1: the accidental full-library import
// Pulls in the ENTIRE lodash library - ~70KB minified
import _ from 'lodash';
_.debounce(fn, 300);
// Pulls in only debounce - ~2KB
import debounce from 'lodash/debounce';This is the single most common finding in every audit we run: a barrel import (`import _ from 'lodash'`, `import * as Icons from 'lucide-react'`) that Webpack can't tree-shake because the whole module gets evaluated for its side effects. Named submodule imports, or a library that ships proper ESM with `sideEffects: false` in its package.json, are the fix.
Pattern 2: duplicate versions of the same dependency
The treemap shows two boxes labeled `lodash` at different sizes - a monorepo package pinned an older lodash than the app, npm's resolution algorithm nested two copies instead of hoisting one, and both shipped to the browser. `npm ls lodash` (or `yarn why`) finds the culprit; a `resolve.alias` pin or a `resolutions`/`overrides` entry in package.json forces a single version.
Pattern 3: a moment library shipping every locale
// moment ships ~150KB of locale data by default
new webpack.ContextReplacementPlugin(
/moment[\\/]locale$/,
/en|es|fr/, // only the locales you actually support
);moment.js (and a handful of other libraries with dynamic locale/plugin loading) bundle every locale unless told otherwise, because Webpack has to statically include everything a dynamic `require()` could possibly resolve to. `ContextReplacementPlugin` narrows that set. The longer-term fix is migrating off moment to date-fns or Temporal, which don't have this problem at all.
Pattern 4: dev-only code leaking into the production bundle
Redux DevTools extensions, verbose logging wrappers, and prop-type validators sometimes end up imported unconditionally instead of behind an `if (process.env.NODE_ENV !== 'production')` check - and Webpack's DefinePlugin can't dead-code-eliminate a branch it doesn't know is guarded by an environment variable unless that variable is actually replaced at build time, not read at runtime from `process.env` directly in every file.
Run the analyzer on every meaningful dependency bump, not just once. Bundle bloat accumulates one 'just add this one package' decision at a time.
Written by Appesto Engineering.