Quick take: The median website ships 500 KB of compressed JavaScript, roughly 3x the recommended 150 KB budget, according to the HTTP Archive's 2025 report. Bloat comes from three sources: dependency sprawl, polyfill debt, and framework overhead. Fix each pillar separately, with bundle analysis, browserslist review, and framework audits, to cut total bundle size in half without touching 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. Last month I audited a client's React e-commerce app. The homepage shipped 1.4 MB of JavaScript. Compressed. The team had no idea it had gotten that bad because the growth happened slowly over two years of feature work. 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. A Next.js 14 app with only 43 direct dependencies in its package.json can easily resolve to over 1,800 installed packages. Each direct dependency pulls in its own tree.
Here's what keeps happening. A developer needs date formatting and installs moment.js (288 KB minified, 72 KB gzipped). Another dev needs a color picker and pulls in react-color, which bundles tinycolor2 plus fourteen React components. 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 replaced lodash entirely in one project and saved 24 KB gzipped from the main bundle.
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.
I updated browserslist in a Vue 3 project from > 1% to > 0.5%, not dead and the Babel output dropped by 31 KB before gzip. That's free performance. No feature changes, no refactoring, just deleting code nobody needed. Here's the actual diff:
{
"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.
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.
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.
| Framework | Production runtime size | Hydration approach |
|---|---|---|
| React 19 | ~55 KB minified + gzipped | Full client re-execution to attach handlers |
| Vue 3.4 | ~33 KB minified + gzipped | Similar hydration model to React |
| Svelte | ~2 KB runtime helpers | Compiles away the framework at build time |
| Astro islands | 0 JS by default | Only hydrates interactive components |
| Qwik | Minimal | Resumability, 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 my audit checklist that takes about fifteen minutes, 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.
- Run a bundle analyzer to find the three largest non-application chunks
- Check Chrome DevTools Coverage tab for unexecuted (polyfill or dead) code
- Compare your framework's runtime size on bundlephobia.com against total bundle size
- 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.
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.
What Practical Cuts Actually Work?
I've applied this framework to six production projects over the past year. The results follow a pattern. Dependency sprawl usually accounts for 40-50% of the excess weight. Polyfill debt adds 15-25%. Framework overhead is the remainder.
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, worth 60 KB or more on a typical build. 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.
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.
Related
- 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