Skip to content

The Three Pillars of JavaScript Bloat and the Fixes

JavaScript bloat comes from dependency sprawl, polyfill debt, and framework overhead. Learn how to measure each pillar and cut your bundle size in half.

· · 9 min read

Updated: July 29, 2026

Network waterfall chart showing large JavaScript bundles loading in a browser DevTools panel

Quick Take

JavaScript bundle bloat almost always comes from three sources, unnecessary dependencies, legacy polyfills targeting modern browsers, and tree-shakeable framework overhead left in. Fixing all three together typically cuts bundle size 40-60% without touching any application logic.

JavaScript bloat is the accumulation of unnecessary bytes in a production bundle, code that ships to the browser but never executes or never needed to exist for that user's device, and it almost always traces back to three separable causes rather than one. The usual story: a homepage ships far more JavaScript than anyone intended, and the team has no idea it got that bad because the growth happened slowly, one reasonable feature at a time. Sound familiar?

JavaScript bloat isn't one problem. It's three distinct problems wearing a trenchcoat. I've started calling them the three pillars because each one requires a different diagnosis and a different fix. Ignore any single pillar and your bundle stays fat no matter how much you optimize the other two.

Pillar One: What Causes Dependency Sprawl?

The HTTP Archive's 2025 State of JavaScript report found the median website now ships 509 KB of compressed JavaScript to desktop users. Mobile gets roughly the same payload on connections that are ten times slower. Where does it all come from? Mostly from packages you didn't realize you installed.

Run npm ls --all in any mid-size project and you'll see hundreds of transitive dependencies. Each direct dependency pulls in its own tree, and a short package.json can resolve to a very long lockfile.

Here's what keeps happening. A developer needs date formatting and installs moment.js (about 294 KB minified and 75 KB gzipped for 2.30.1, per Bundlephobia). Another dev needs a color picker and pulls in a package with its own dependency tree. Nobody checks the cost. Why would they? The install takes two seconds.

The fix starts with visibility. Run npx source-map-explorer build/static/js/*.js after a production build and you'll see exactly which packages eat the most space. Swap moment.js for date-fns (tree-shakeable, you only import what you use). Replace lodash with lodash-es or just write the three utility functions you actually need. I'd rather own three small functions than carry a utility belt for them.

Bundlephobia.com should be a mandatory check before every npm install. It shows minified size, gzipped size, download time on 3G, and whether the package supports tree-shaking. Treat it like a code review gate.

Pillar Two: What Is Polyfill Debt?

According to web.dev's modern JavaScript guide, shipping transpiled and polyfilled code for browsers that represent under 0.5% of your traffic is one of the largest sources of unnecessary bytes. This is polyfill debt, code your users' browsers don't need.

Babel's @babel/preset-env with useBuiltIns: 'usage' was supposed to fix this. It helps. But many projects still target IE 11 in their browserslist config even though Microsoft killed IE in June 2022. That single line forces Babel to polyfill Promises, Array.from, Object.assign, Symbol, and dozens of other features that every modern browser has supported for years.

Check your .browserslistrc right now. If it says > 0.5%, last 2 versions, not dead, you're probably fine. If it includes ie 11 or chrome 49, you're shipping polyfill code to 99% of your users who don't need it.

Tightening the target from > 1% to > 0.5%, not dead is free performance when your old config was stricter than your real audience. No feature changes, no refactoring, just deleting code nobody needed. Here's the shape of the change:

{
  "browserslist": ["> 1%"]
}
{
  "browserslist": ["> 0.5%", "not dead", "not ie 11"]
}

Does your team even know which browsers you officially support? Most don't. That's the root cause.

A room overwhelmingly cluttered with piles of papers, boxes, and objects
Photo by Nechirwan Kavian on Unsplash

Pillar Three: What Is Framework Overhead?

This is the controversial one. Frameworks ship runtime code that executes before your application logic even starts. React 19's gzipped runtime landed around 55 KB, up from React 18's roughly 44 KB, mostly because the React Compiler's automatic memoization machinery has to ship with it. Vue 3.4 comes in around 33 KB. Svelte compiles away its framework at build time and ships roughly 2 KB of runtime helpers.

That React 19 number is worth sitting with. You add the React Compiler specifically to stop hand-writing useMemo and useCallback, and the tradeoff is a heavier baseline bundle every single page has to download before any memoization benefit shows up. It's a fair trade for a large interactive app doing lots of re-renders. It's a bad trade for a content site that just needs one accordion to open and close.

I think most SPAs don't justify their framework cost. There, I said it. If your "app" is really a content site with a contact form and a hamburger menu, you don't need 42 KB of React runtime plus 15 KB of React DOM hydration code just to make a button interactive. But try telling that to a team that's already committed to React 19.

Most SPAs don't justify their framework cost. If your "app" is really a content site with a contact form and a hamburger menu, you don't need 42 KB of React runtime just to make a button interactive.

Share this Post on X Bluesky

Hydration is the hidden tax. The server sends HTML, then the browser downloads JavaScript, parses it, and re-executes component logic to attach event handlers. On a median mobile device (a Moto G Power running Android 12), that hydration step can block interactivity for 2-4 seconds on a complex page.

Newer patterns help. React Server Components reduce client JavaScript. Astro's islands architecture ships zero JS by default and only hydrates interactive components. Qwik takes a radical approach with resumability, no hydration at all. These aren't silver bullets, but they attack the framework pillar directly.

How Do the Framework Runtime Costs Compare?

Framework runtime is the code that ships before you write a single line of your own, and the spread across popular choices is wider than most teams assume. React 19 costs roughly 55 KB minified and gzipped once you count react plus react-dom. Svelte lands near 2 KB, because its compiler turns components into direct DOM instructions at build time and leaves almost no framework behind to download. That's a 27x difference on the baseline. The number to weigh it against is your total bundle: 55 KB inside a 400 KB app is noise, while the same 55 KB inside a 90 KB marketing site is most of your budget. Hydration approach matters just as much as size, since re-executing your whole component tree on the client costs main-thread time no gzip setting will recover.

FrameworkProduction runtime sizeHydration approach
React 19~55 KB minified + gzippedFull client re-execution to attach handlers
Vue 3.4~33 KB minified + gzippedSimilar hydration model to React
Svelte~2 KB runtime helpersCompiles away the framework at build time
Astro islands0 JS by defaultOnly hydrates interactive components
QwikMinimalResumability, no hydration step at all

How Do You Measure All Three Pillars at Once?

You can't fix what you can't see. Here's the audit checklist I'd run, in this order:

The order isn't arbitrary. Dependency sprawl comes first because it's where the biggest single wins hide, one date library swapped for a 2 KB alternative can beat a week of careful code-splitting. Polyfills come second since they're usually a one-line config change once you know your real browser support floor. Framework overhead comes last because it's the only item on the list that might mean rewriting something, and you should never reach that conversation before the cheap fixes are done. Run all 3 checks in one sitting rather than spreading them across sprints, because the numbers only make sense relative to each other, and a chunk that looks alarming in isolation often turns out to be 4% of the total.

  1. Run a bundle analyzer to find the three largest non-application chunks
  2. Check Chrome DevTools Coverage tab for unexecuted (polyfill or dead) code
  3. Compare your framework's runtime size on bundlephobia.com against total bundle size
  4. Prioritize fixes by size, dependency sprawl first, then polyfills, then framework overhead

Bundle Analysis

Run npx webpack-bundle-analyzer stats.json or npx vite-bundle-visualizer depending on your build tool. Look for the three biggest non-application chunks. Those are your dependency sprawl candidates. A one-off analysis only helps until the next dependency bump, so pin the numbers you get here in CI with a size budget, as shown in how size-limit catches bundle regressions.

Polyfill Check

Open Chrome DevTools, go to the Coverage tab, and load your page. Red bars mean unexecuted code. If 40% of your main bundle is red, you've got polyfill debt or dead code or both.

Framework Cost

Check your framework's runtime size on bundlephobia.com. Compare it against your total bundle. If the framework runtime is more than 30% of your JavaScript, you might be over-framed for the job. A blog doesn't need what a SaaS dashboard needs.

Three colorful pairs of scissors arranged on a cream background
Photo by Natalia Blauth on Unsplash

What Practical Cuts Actually Work?

Your split between the three pillars will differ from anyone else's, which is why the audit comes first. I'd expect dependency sprawl to dominate in an app with a long package.json and framework overhead to dominate on a small content site, but that's a guess until your analyzer says otherwise.

Start with the biggest pillar first. Swap heavy packages for lighter alternatives, moment for date-fns or the native Intl API is still the highest-yield trade I know of, since moment alone is about 75 KB gzipped. Update your browserslist, and be honest about the floor rather than copying a config from 2019. Consider whether partial hydration or server components could reduce your framework footprint. And check what each remaining package is actually there for: a JavaScript scroll library that CSS now replaces outright is dependency sprawl and framework overhead billed twice for a progress bar.

The uncomfortable truth? Most JavaScript bloat isn't a technical problem. It's a cultural one. Teams add dependencies without checking costs, never update their browser targets, and pick frameworks based on hiring rather than fit. Fix the culture and the bundles follow.

  • Frontend ideas that keep returning - bloat runs on a cycle, and the same ideas return under new names once the pendulum swings
  • Node.js 20 to 24 migration - upgrading Node.js removes built-in polyfills and changes which globals exist; major version bumps directly affect your polyfill debt pillar
  • Blocking the Internet Archive Won't Stop AI Crawlers - web preservation and AI crawling are reshaping how JavaScript practices are documented and trained on
  • React 19: Complete Guide to New Features and Migration - the React Compiler reduces the client-side JS footprint, directly attacking the framework overhead pillar
  • Lucide React - 1500+ SVG icons at ~1kB per icon via tree-shaking; the right way to add icons without dragging in a 200kB icon font

Frequently Asked Questions

How do I measure JavaScript bloat on my site?
Run Lighthouse in Chrome DevTools or use bundlephobia.com for individual packages. For full audits, webpack-bundle-analyzer or source-map-explorer will visualize exactly which modules consume the most bytes in your production bundle.
What is an acceptable JavaScript bundle size?
Alex Russell's performance budget suggests under 150 KB of compressed JavaScript for a good mobile experience on median devices. The median website ships around 500 KB compressed, which is roughly three times the recommended budget.
What is the fastest way to reduce JavaScript bundle size?
Start with bundle analysis to find the biggest offenders. Replace heavy libraries with lighter alternatives, like date-fns instead of moment.js. Then enable tree-shaking, remove unused polyfills, and consider lazy loading routes that users don't hit on first page load.
Does tree-shaking automatically remove all unused code?
Tree-shaking removes unused exports only from ES modules with side-effect-free code. CommonJS modules (require/module.exports) don't tree-shake. Libraries without a sideEffects: false field in package.json may still ship unused code. Verify in bundle analyzer after enabling tree-shaking, some imports that look unused still carry side effects and won't be removed.
What are the most common causes of excessive JavaScript load time?
The three main causes are large vendor bundles (importing entire libraries instead of specific functions), render-blocking third-party scripts (analytics, chat widgets, A/B testing), and missing code splitting (all routes bundled into one file). A fourth issue is polyfills shipped to modern browsers that don't need them, use @babel/preset-env with browserslist targets to avoid this.