Quick take:, The median website ships 500 KB of compressed JavaScript, roughly 3x the recommended 150 KB budget (HTTP Archive, 2025). Bloat comes from three sources: dependency sprawl, polyfill debt, and framework overhead. Fix each pillar separately to cut bundle size in half, fixes is covered here.
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: 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. I once counted 1,847 packages in a Next.js 14 app that only had 43 direct dependencies in its package.json. 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: 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: Framework Overhead
This is the controversial one. Frameworks ship runtime code that executes before your application logic even starts. React 18.2's production runtime is about 42 KB minified and gzipped. Vue 3.4 comes in around 33 KB. Svelte compiles away its framework at build time and ships roughly 2 KB of runtime helpers.
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.
Measuring All Three Pillars at Once
You can't fix what you can't see. Here's my audit checklist that takes about fifteen minutes:
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.
Practical Cuts That 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. Update your browserslist. 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. On a related note, web preservation becomes increasingly important as the Internet Archive documents the evolution of JavaScript practices, see our analysis of web preservation AI training.
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