Understanding bundle analysis
What's actually in your JavaScript.
The treemap visualisation behind every bundle analyser, the three categories of fat, and the actions each suggests.
The treemap visualisation.
Every modern bundle analyser (webpack-bundle-analyzer, rollup-plugin-visualizer, source-map-explorer) draws a treemap: each rectangle is a module, its area is proportional to its byte count, nested rectangles show parent-child relationships. A 2MB bundle reveals at a glance: this 600KB rectangle is React + ReactDOM, this 400KB is Moment.js, this 200KB is one analytics library. The visual hierarchy makes "what's making my bundle big" instantly answerable.
The three categories of fat.
One: oversized libraries with cheaper alternatives. Moment.js (300KB) → date-fns (15KB used). Lodash (full) → individual lodash-es imports or native methods. Material UI (500KB+) → Radix UI primitives. Each swap is a 1-day project and saves 100KB+. Two: duplicate dependencies. Two versions of the same library because deps disagree on the version range. Webpack's resolve.alias or pnpm's strict hoisting fixes this. Three: code that doesn't need to be in the main bundle — route-specific code, modals, admin panels. Code-split with import().
A worked analysis.
A bundle reports 1.8MB gzipped. Treemap shows: React + ReactDOM 130KB, Recharts 110KB (only used on /dashboard), Moment.js 80KB (one date format string), an admin-panel component tree 200KB (used in 2 % of sessions), plus the actual app. Actions: code-split Recharts behind the /dashboard route lazy load (-110KB); swap Moment for native Intl (-80KB); code-split admin (-200KB from main bundle). Result: 1.8MB → 1.4MB gzipped, 22 % smaller, applied in ~half a day.
Three wins
lazy-load + swap + code-split
Each lever knocks 80-200KB off the initial bundle.
-110 (recharts) -80 (moment) -200 (admin) = -390KB
= 1.8MB → 1.4MB initial
What "the bundle size" actually is.
Three numbers matter, in order. Initial JS (first-paint cost): the script tags in your initial HTML. Should be under 200KB gzipped for mobile-first apps. Route chunks: code-split chunks loaded per route; ideally each under 100KB. Total: everything across the site. The first matters most; the others matter only when users navigate. A common mistake is optimising "total bundle size" without realising the initial load is what affects Core Web Vitals.
Source-map-explorer is the cheapest.
For projects without bundler-integrated analysers, npx source-map-explorer dist/assets/*.js reads the source maps you already produce and emits the same treemap. Zero setup; works with any bundler. The bundler-native analysers (webpack-bundle-analyzer, rollup-plugin-visualizer) integrate into the build and run automatically; source-map-explorer runs on demand. Pick the workflow that matches your discipline.
Bundle-size budgets.
The discipline that stops bundle bloat: set explicit per-bundle byte budgets in CI, fail the build if exceeded. size-limit is the de-facto tool. A budget of "initial JS ≤ 180KB gzipped" forces every PR to either stay under or explain (re-budget, code-split, drop a dep). Without budgets, bundle size grows quietly; with them, every commit that adds weight is visible before merge.