Back to the blog
Webpack· Jul 15, 2026· 7 min read

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#performance#bundle-size#tooling

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.