Your JavaScript Bundle Is Fat Because of a Decision You Made 18 Months Ago

You run the bundle analyzer. The treemap appears — a patchwork of rectangles in different colors, each one a dependency, most of them larger than you expected. You hover over something called core-js/modules taking up 140KB and feel vaguely accused.
So you spend an afternoon tweaking webpack config, adding dynamic imports, moving things around. The bundle gets 12KB smaller. You close the laptop satisfied.
Six months later, it's back to where it was.
The bundle analyzer is not lying to you. But it's showing you the wrong thing. It shows you the symptom. The cause is somewhere else — and it's probably somewhere you stopped looking.
The Bundle Analyzer Shows You the Symptom, Not the Cause
Every tutorial on JavaScript performance starts at the same place: "run your bundle analyzer and see what's taking up space." This is like diagnosing a fever by reading the thermometer.
The thermometer is accurate. It doesn't tell you what's causing the fever.
The bundle analyzer shows you what's large. It doesn't explain why that thing is there, when it was added, which transitive dependency pulled it in, or whether any of the code actually runs in production. It's a map of the symptom.
The actual cause of most JavaScript bloat fits into three categories. They're all architectural decisions. None of them are visible in the bundle analyzer until it's too late.
Legacy Compatibility Is Doing Most of the Damage
core-js. Regenerator runtime. The full Babel polyfill chain. These are the first place to look when a JavaScript bundle is meaningfully larger than it should be.
The problem is generational. Four to six years ago, supporting IE11 was a real requirement. The standard configuration for any production app included a polyfill suite that ported ES2015+ features back to environments that didn't support them. These configurations were copied between projects, baked into create-react-app defaults, committed to company starter kits, and passed on to new developers who inherited them without questioning why they were there.
IE11's global market share is effectively zero. Its share of traffic to most software products has been zero for years. But the polyfill configuration from 2020 is still sitting in babel.config.js, still being included in every build, still targeting a browser that no longer exists.
The impact is not trivial. A full core-js polyfill suite targeting ES5 can add 40 to 60KB to a parsed and minified bundle. Combined with Babel's regenerator runtime for async/await support (which modern browsers have handled natively since 2017), you're looking at 80 to 100KB of compatibility infrastructure for users who don't need any of it.
The fix is brutal in its simplicity: run npx browserslist in your project and look at what your current target actually includes. If IE11 is on the list and nobody put it there deliberately, remove it. Update your Babel configuration to target the actual browsers your users use. The polyfills go with it.
This single change — not a webpack optimization, not code splitting, not lazy loading — is often responsible for 15 to 25 percent of the total bundle size.
The Micro-Package Trap
The JavaScript ecosystem has a packaging philosophy that rewards atomicity. Small packages, single responsibility, import exactly what you use. In theory, tree-shaking makes this safe: import one function from a ten-function library, and the bundler discards the other nine.
In practice, tree-shaking is less effective than advertised.
Consider the moment.js to date-fns migration that happened across the ecosystem around 2019 to 2021. Moment.js was 71KB minified and gzipped. date-fns was modular and tree-shakeable. The migration happened. But date-fns brings along locale files. And if your project has any dynamic locale loading — which many do, because internationalization — the tree-shaker can't statically analyze the import. You end up with 60KB of locale data for twelve languages your users don't speak.
This is the pattern. You import a chart library. The chart library depends on a color utility. The color utility depends on a string formatter. The string formatter has optional locale support. Somewhere in that chain, a dynamic import breaks the static analysis. The bundler includes more than it should. You don't notice until the treemap turns up a rectangle labeled "something/vendor/locale" that you've never heard of.
The audit that catches this is not the bundle analyzer. It's npm ls or pnpm why — commands that show you why a package is in your dependency tree at all. Running these regularly on a production codebase is not glamorous work. It's also the only way to catch dead dependencies before they become weight you carry indefinitely.
The Framework Lock-in You Didn't Notice
The third architectural source of bloat is the framework version and its associated ecosystem.
React 17 applications that include the React DevTools compatibility shim in production builds carry an extra 8 to 12KB they shouldn't. Next.js projects that haven't migrated away from getInitialProps — a pattern deprecated in Next.js 10, still found in many codebases because migration requires touching dozens of files — carry a server-side rendering bundle strategy that includes more runtime than the pages need.
Vue 2 projects that haven't been upgraded carry a compatibility layer for Vue 2's reactivity system that Vue 3 eliminated. Angular projects configured to include zone.js — default in Angular, removable since Angular 14 — carry 34KB of change-detection infrastructure that Signals-based components no longer require.
These aren't just theoretical cases. They're production codebases everywhere, carrying framework baggage from decisions made at project initialization and never revisited. The framework changelog says "you can remove this." The team never gets around to it.
The meta-pattern here: the biggest contributors to JavaScript bloat are almost always things that were correct at the time they were added. Polyfills were necessary. Framework defaults were reasonable. Package choices matched the available ecosystem. The mistake isn't the original decision — it's the failure to audit those decisions as the landscape changed.
How Bundle Reduction Actually Happens
The teams that maintain consistently lean JavaScript bundles don't run the bundle analyzer more often. They have a different process.
They treat the dependency manifest as a first-class artifact. package.json gets reviewed in pull requests the same way application code does. A new dependency requires justification: what does it replace, what's its bundle impact, what are its sub-dependencies.
They track bundle size as a metric with a budget and an alert. Not a one-time check — a CI step that fails the build when a new PR causes the bundle to exceed its budget. This sounds aggressive until you've watched a 30KB third-party analytics package get added to a project with no discussion and no visibility because nobody was watching.
They do quarterly dependency audits. Not security audits — those are automated. Architecture audits: what's still pulling in that old polyfill suite, what packages haven't been updated in two years, what was added for a feature that got removed six months ago.
The bundle size problem isn't a bundler problem. It's a process problem. The configuration that optimizes bundles most effectively is the one that prevents unnecessary code from entering the dependency tree in the first place.
What to Audit First
If you're looking at a bloated JavaScript bundle right now and trying to decide where to start:
Check your Babel targets. Run npx browserslist and see what you're supporting. Remove IE11 if it's there with no business justification. Reconfigure core-js imports to use useBuiltIns: 'usage' rather than entry if you haven't already.
Check your locale data. Moment-timezone, date-fns locale files, i18n libraries with bundled translations — these are reliably among the heaviest items in any bundle that includes them naively. Lazy-load locales or bundle only what your actual user base needs.
Run pnpm why or npm ls on your heaviest bundle items. Find out which of your direct dependencies is pulling them in. Ask whether that direct dependency is still necessary.
Check your framework version and changelog for bundle-related deprecations. There's probably something in there that your project hasn't acted on.
None of this is the bundle analyzer. All of it is the actual cause.
Related: AI Writes the Code. You Review It. The Debt Ships Anyway. — on the broader pattern of technical debt that accumulates before anyone notices.
Photo by pipop kunachon via Pexels.