JavaScript Bundle Analyzer: Find Bloat Without Guessing

2026-03-15 · Nico Brandt

Your production bundle is 1.8 MB. You added one library last week and it jumped 400 KB. You have no idea which dependency is responsible.

Most developers never look inside their bundle. They keep adding packages until the Lighthouse score turns red — one symptom when you’re investigating why your website is slow — then start guessing, removing things one at a time, rebuilding, checking if the number moved. That’s not debugging. That’s whack-a-mole.

A javascript bundle analyzer shows you exactly what’s in there. Five minutes of setup. Zero guessing.

What a Bundle Analyzer Actually Shows You

A bundle analyzer generates an interactive treemap — a nested rectangle visualization where every module in your bundle gets a box sized proportionally to its weight. The bigger the box, the bigger the problem.

Three size metrics show up in these tools:

The treemap makes invisible problems visible. That date library you imported for one formatting function? It’s a giant rectangle. The two copies of lodash at different versions your dependency tree pulled in? They’re sitting side by side, both eating space. The CSS-in-JS runtime you didn’t know your component library shipped? It’s there, quietly doubling your parsed size.

Once you see it, you can’t unsee it. Setting up the analyzer takes less time than your last npm install.

Setup: Webpack, Vite, and Rollup

Every major bundler has an analyzer plugin. Here’s each one — all under five minutes.

Webpack

Install the plugin:

npm install --save-dev webpack-bundle-analyzer

Add it to your webpack.config.js:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

Run your build. The treemap opens in your browser automatically.

If you don’t want to touch config, use the CLI mode instead. Generate a stats file and feed it to the analyzer separately:

npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json

Same treemap, zero config changes. Good for one-off audits on projects you don’t own.

Vite

Vite uses Rollup under the hood, so the analyzer is a Rollup plugin:

npm install --save-dev rollup-plugin-visualizer

Add it to vite.config.ts:

import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({ open: true })
  ]
});

Run npm run build. A stats.html file opens with your treemap — same interactive visualization, different bundler.

Rollup

Same package, same API:

npm install --save-dev rollup-plugin-visualizer

In rollup.config.js:

import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer({ open: true })
  ]
};

Three bundlers, three setups, none longer than five lines of config. You’ve got a screen full of colored rectangles now. The hard part isn’t generating the treemap — it’s knowing what to do with it.

Reading the Treemap: 4 Bloat Patterns to Spot Immediately

Every bloated bundle has the same handful of problems. Here’s what to look for.

Pattern 1: The giant you didn’t invite. One dependency takes up 30% or more of your bundle. Moment.js, the full lodash build, Material UI’s entire component set. If a single box dominates the treemap, that’s your first target. You probably use 5% of what it ships.

Pattern 2: Duplicates. Two copies of the same library at different versions. This happens silently when your dependencies depend on conflicting versions of a shared package. Look for repeated names at slightly different sizes. Lodash, core-js, and tslib are repeat offenders.

Pattern 3: Tree-shaking failures. You imported one function. The entire library shipped. This happens when a package publishes CommonJS instead of ES modules — bundlers can’t tree-shake require() calls. The treemap shows the full package even though you used formatDate and nothing else. If a box looks way too big for what you actually import, this is probably why.

Pattern 4: Transitive bloat. A small utility you installed pulls in a massive dependency you never asked for. That lightweight “markdown parser” depends on a full HTML sanitizer that depends on a DOM implementation. Check what’s nested inside surprisingly large blocks — the real culprit is often two levels deep.

Spotting bloat is satisfying. Fixing it is where the bundle size actually changes.

Fix What You Found: A Concrete Playbook

Each pattern has a specific fix. Work the biggest box first — largest rectangle, largest win.

Giant dependencies → replace or trim. Swap moment.js for date-fns (or the Intl API if you only need formatting). Replace the full lodash import with per-method imports: import debounce from 'lodash/debounce' instead of import { debounce } from 'lodash'. That single change can drop hundreds of kilobytes.

Duplicates → deduplicate. Run npm ls <package-name> to see where the versions diverge. Then:

npm dedupe

If that doesn’t resolve it, pin the shared version in your package.json overrides (npm) or resolutions (Yarn). Force one copy.

Tree-shaking failures → switch to ESM. Check if the library publishes an ES module build — look for a module or exports field in its package.json. Make sure you’re using named imports: import { thing } not import * as lib. Avoid barrel files that re-export everything — they defeat tree-shaking by referencing every module in the package.

Transitive bloat → code-split. Use dynamic import() to lazy-load heavy sub-trees that only trigger on user interaction. No reason to pay upfront for something the user might never click. If you’re in React, React.lazy with Suspense handles this cleanly — our guide on lazy loading best practices covers the patterns that actually improve LCP instead of hurting it.

The rule after every fix: re-run the analyzer. Verify the box shrank or disappeared. If it didn’t move, your fix didn’t land.

The 60-Second Bundle Audit Checklist

Your bundle was 1.8 MB and you didn’t know why. Now you do.

Run this after every major dependency change:

  1. Build with the analyzer plugin enabled
  2. Check for giants — any single dependency over 30% of total
  3. Check for duplicates — repeated package names at different sizes
  4. Check tree-shaking — boxes too large for what you actually import
  5. Check transitive bloat — small packages with unexpectedly large nested deps
  6. Fix the largest offender first
  7. Re-run. Verify the box is gone

For extra credit, add the analyzer to CI. Both webpack-bundle-analyzer and rollup-plugin-visualizer can output JSON — pipe it into a size budget check and catch regressions before they merge. That pairs well with the other pre-ship checks in our web performance checklist.

The goal isn’t a perfect bundle. It’s knowing exactly what’s in yours and making deliberate choices about what stays. That’s the difference between a 1.8 MB bundle you’re worried about and a 1.8 MB bundle you can defend.