Skip to content
All posts
PerformanceSEOConversionJune 11, 2026

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:

MeasureWhat it isTypical ratio
Raw (source) sizeThe unminified code with comments and whitespace1×
Minified sizeWhitespace and comments removed, identifiers shortened~0.4–0.6×
Compressed (gzip or Brotli) transfer sizeWhat 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:

  1. Transfer. Bytes over the network, helped by compression and a CDN, and paid again on every uncached visit.
  2. 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-analyzer renders a treemap of every module and its size.
  • Next.js: @next/bundle-analyzer wraps the same thing for each route.
  • Vite and Rollup: rollup-plugin-visualizer.
  • Any source map: source-map-explorer works 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.

LibraryApprox. compressed sizeNote
React 18 + ReactDOM~45 KBBaseline for any React app
Preact~4 KBReact-compatible API, fraction of the size
Vue 3 (runtime)~35 KB
Svelte (runtime)a few KBCompiles components to plain JS
jQuery 3.7~30 KBOften loaded for one selector
Lodash (full)~25 KBPer-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 KBTree-shakeable
A typical chart library50–150 KBLoad only on pages that draw charts
A tag manager, chat widget or A/B tool100–300 KB eachUsually 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:

BeforeAfter
Compressed JS on the page480 KB210 KB
Main-thread execution (mid-range phone)1.9 s0.8 s
Interaction to Next Paint (p75)340 ms170 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.

Run the free check

Want to see how your website scores?

Get Your Growth Score