JavaScript Bundle Size Explained: What It Means, How to Measure It, and How to Reduce It
"Bundle size" is the amount of JavaScript your page makes the browser download, parse and run before it works the way you intended. It is the single biggest reason modern sites feel slow on phones, it moves two of the three Core Web Vitals, and it is the metric that quietly decides whether a checkout button responds when someone taps it. This guide explains what the number actually measures, what a sensible budget looks like, how to measure your own, and the eight techniques that reduce it most.
What bundle size means
When you build a modern web app, a bundler (webpack, Vite, Rollup, esbuild, Turbopack) takes your source files and every library they import and produces one or more JavaScript files, the bundles, that the page loads. "Bundle size" is the size of those files, and it is quoted in three different ways, which is where confusion starts:
| Measure | What it is | Typical ratio |
|---|---|---|
| Raw (source) size | The unminified code with comments and whitespace | 1× |
| Minified size | Whitespace and comments removed, identifiers shortened | ~0.4–0.6× |
| Compressed (gzip or Brotli) transfer size | What actually crosses the network | ~0.25–0.35× of raw |
When someone says "our bundle is 300 KB", ask which. A 300 KB compressed bundle is roughly a megabyte of code for the browser to parse.
That distinction matters because JavaScript has two costs, and only one of them is download time:
- Transfer. Bytes over the network, helped by compression and a CDN, and paid again on every uncached visit.
- Execution. The browser has to parse, compile and run the code on the main thread, the same thread that handles taps, scrolls and rendering. Compression does nothing for this cost, and a mid-range Android phone does it several times slower than a laptop.
That is why a kilobyte of JavaScript is more expensive than a kilobyte of image. An image decodes off the main thread and then sits there; a script has to be executed before the page is interactive, and it may keep running.
Why it matters
Speed. Large bundles delay the moment the page becomes usable. Lighthouse reports this as Total Blocking Time (lab) and the real-user metric is Interaction to Next Paint: when the main thread is busy running your bundle, a tap on a button waits. Bundles also compete with the hero image for bandwidth and delay Largest Contentful Paint when they are render-blocking or when the framework renders the hero client-side.
Conversion. The 2020 study by Google and Deloitte, Milliseconds Make Millions, measured that a 0.1-second improvement in mobile site speed lifted conversion rates by around 8% for retail sites. Speed is the part of the funnel that runs before any copy is read.
Cost. Every byte is paid for by the visitor's data plan and battery, and by your CDN bill.
Scale of the problem. The HTTP Archive's Web Almanac puts the median page at well over 500 KB of compressed JavaScript on mobile, with the heaviest tenth of pages above a megabyte. Most of that is not the application: it is frameworks, polyfills for browsers nobody uses any more, and third-party tags.
What is a good bundle size? Budgets that hold up
There is no universal number, but there is a widely used starting point: about 300 KB of compressed JavaScript per page on mobile keeps a mid-range phone on a 4G connection interactive within a few seconds. Above that, every additional 100 KB is felt. Sites built with server rendering, islands or static generation routinely ship 50–150 KB; single-page apps with a full framework and a few libraries ship 300–800 KB before anyone notices.
A budget is only useful if something enforces it. Set one per route ("the product page ships no more than 200 KB compressed") and make the build fail when it is exceeded (see the CI section below).
How to measure bundle size
1. Chrome DevTools, Network tab. Filter by JS, reload with cache disabled, and read the Size column, which shows transferred (compressed) size, and the Resource column, which shows the uncompressed size. The status bar at the bottom totals them.
2. DevTools Coverage tab. Open it from the Command Menu ("Show Coverage"), reload, and it lists every script with the percentage of bytes that were never executed during the load. Unused code is the cheapest thing to remove.
3. Lighthouse / PageSpeed Insights. Three audits target bundles directly: Reduce unused JavaScript, Reduce JavaScript execution time and Avoid enormous network payloads. A fourth, Avoid serving legacy JavaScript to modern browsers, catches polyfills you no longer need. PageSpeed also shows which third-party origins cost the most main-thread time.
4. A bundle analyzer. This is the tool that shows what is in the bundle:
- webpack:
webpack-bundle-analyzerrenders a treemap of every module and its size. - Next.js:
@next/bundle-analyzerwraps the same thing for each route. - Vite and Rollup:
rollup-plugin-visualizer. - Any source map:
source-map-explorerworks with the output of any bundler.
The treemap makes the usual culprits obvious: a date library with every locale, a full icon set imported for three icons, a charting library on a page with no charts.
5. Bundlephobia (bundlephobia.com) shows the minified and compressed cost of any npm package before you add it, plus whether it is tree-shakeable. Check every dependency here before installing it.
6. In CI. size-limit, bundlesize and Lighthouse CI compare each build against your budget and fail the pull request that breaks it. A budget that is only checked by hand is a budget nobody checks.
A bundle size chart: what common libraries cost
Approximate minified + gzipped sizes, rounded, as reported by Bundlephobia; check the current version before deciding.
| Library | Approx. compressed size | Note |
|---|---|---|
| React 18 + ReactDOM | ~45 KB | Baseline for any React app |
| Preact | ~4 KB | React-compatible API, fraction of the size |
| Vue 3 (runtime) | ~35 KB | |
| Svelte (runtime) | a few KB | Compiles components to plain JS |
| jQuery 3.7 | ~30 KB | Often loaded for one selector |
| Lodash (full) | ~25 KB | Per-function imports cut this to 1–3 KB |
| Moment.js with all locales | ~70 KB+ | Replace with date-fns, Day.js or the Temporal API |
| date-fns (a few functions) | ~2–5 KB | Tree-shakeable |
| A typical chart library | 50–150 KB | Load only on pages that draw charts |
| A tag manager, chat widget or A/B tool | 100–300 KB each | Usually the largest scripts on the page and not yours |
Eight ways to reduce bundle size
1. Remove or replace heavy dependencies
Run the analyzer and sort by size. The biggest wins are almost always a handful of libraries doing something small. Replace Moment with date-fns or Day.js; import Lodash functions individually (import debounce from 'lodash/debounce'); use the platform where it has caught up (fetch instead of axios, Intl for formatting, CSS for animations that used to need a library). Check the cost of every new package on Bundlephobia before adding it.
2. Make tree shaking work
Tree shaking removes exports nothing imports, but only when the code is ES modules and the bundler can prove there are no side effects. Use libraries that ship ESM, set "sideEffects": false in your own packages where true, and avoid "barrel" files (index.js that re-exports everything) in large internal libraries; bundlers often cannot see through them and pull in the whole folder.
3. Split the code by route and by interaction
The user should download the code for the page they are on, not the whole application. Route-based splitting is built into Next.js, Nuxt, SvelteKit and Remix; with plain webpack or Vite use dynamic import():
// Load the chart library only when a chart is about to render
button.addEventListener('click', async () => {
const { renderChart } = await import('./charts.js');
renderChart(data);
});
Anything behind a click (modals, editors, maps, video players, checkout steps) is a candidate for a dynamic import.
4. Defer third-party scripts, or drop them
Tag managers, chat widgets, session recorders, A/B testing tools and social embeds are usually the heaviest scripts on a page and the ones with the least oversight. On our own homepage, before a clean-up, a chat widget, analytics and a sign-in library added up to about 570 KB of JavaScript before a byte of application code loaded. Audit each tag: does it earn its cost? Load what remains after the page is interactive, on first interaction, or only on the pages that use it, and never in the <head> synchronously.
5. Stop shipping legacy JavaScript
If your build transpiles to ES5 and injects polyfills for Internet Explorer, you are sending every modern browser code it does not need. Set a current browserslist (for example defaults, not dead), let the bundler emit ES2020+ syntax, and drop core-js polyfills that no supported browser requires. Lighthouse's legacy-JavaScript audit shows how much this saves; 20–30% is common.
6. Minify and compress, and check that you actually do
Minification should be on in every production build (Terser, esbuild, SWC). Compression is a server or CDN setting: Brotli typically beats gzip by 15–20% on JavaScript. Verify with a request:
curl -sI -H 'Accept-Encoding: br, gzip' https://example.com/app.js | grep -i content-encoding
content-encoding: br
No content-encoding header means the file is going over the wire uncompressed, which is a one-line fix worth more than weeks of code golf.
7. Ship less JavaScript by design
The largest reductions come from architecture, not tuning. Server rendering, static generation, React Server Components, and "islands" frameworks such as Astro and Qwik send HTML that works without JavaScript and hydrate only the interactive parts. A marketing site does not need a client-side router; a blog does not need a framework runtime at all. Choosing the right tool for the page often removes 200 KB in one decision.
8. Set a budget and enforce it in CI
Decide the compressed limit per route, add size-limit or Lighthouse CI to the pipeline, and fail the build when a pull request exceeds it. Watch the field numbers too: real-user INP and LCP in the Chrome UX Report (which a free audit reports next to the lab data) tell you whether the reduction reached your visitors.
How modern SDKs cut their footprint (and what to copy)
Payment, analytics and mapping SDKs have been on the same diet, and their changelogs are a useful playbook: split a monolithic script into modules so an integration imports only the flows it uses; ship ESM builds so tree shaking removes the rest; load rarely used flows (a specific payment method, an address autocomplete) with dynamic imports on demand; and cut dependencies that duplicated what the browser now does natively. Applied to a typical checkout page, the pattern looks like this:
| Before | After | |
|---|---|---|
| Compressed JS on the page | 480 KB | 210 KB |
| Main-thread execution (mid-range phone) | 1.9 s | 0.8 s |
| Interaction to Next Paint (p75) | 340 ms | 170 ms |
The figures are illustrative, not a specific vendor's data, but the shape is what teams report when they move from one big script to modular, lazy-loaded pieces: less than half the bytes, roughly half the execution time, and an INP that moves from "needs improvement" to "good".
Checklist
- Know your per-route compressed JS size and where it comes from (analyzer treemap).
- Nothing over ~300 KB compressed on mobile without a written reason.
- Heavy libraries replaced or loaded on demand; per-function imports for utility libraries.
- ESM everywhere; tree shaking verified by removing an import and watching the size drop.
- Route and interaction code splitting in place.
- Third-party tags audited, deferred, or removed.
- Modern browserslist; no polyfills for dead browsers.
- Brotli on, verified with curl.
- Budget enforced in CI; field INP and LCP monitored.
Frequently asked questions
What is a good JavaScript bundle size? Under about 300 KB compressed per page on mobile is a defensible target; under 150 KB is where sites start to feel instant on mid-range phones. Measure compressed transfer size, not raw source.
Does bundle size affect SEO? Indirectly and measurably: it drives Interaction to Next Paint and often Largest Contentful Paint, which are Core Web Vitals, a Google ranking signal. It also decides whether Googlebot's renderer sees your content within its budget.
Gzip or Brotli? Brotli, wherever the server or CDN supports it (almost all do). It compresses JavaScript 15–20% smaller than gzip at similar decompression cost.
Is code splitting always better? Splitting too finely creates dozens of small requests and waterfalls of dependent chunks. Split by route and by meaningful interaction, and preload chunks you know will be needed next.
Why is my bundle large when my code is small? Because most of it is not your code. Open the analyzer: the framework, a date or icon library, polyfills and third-party tags usually account for 70–90% of the bytes.
Start with the analyzer treemap and the Coverage tab; between them they will show you the 20% of the bundle that costs 80% of the time. Then run a free performance check to see the before and after in real-user Core Web Vitals.
Check your own site
Free Website Speed Test
See your LCP, CLS and INP on mobile and desktop, with the fixes ranked by payoff.
More from the blog
Want to see how your website scores?
Get Your Growth Score