skip to content

Bundle Budgets

A budget turns "keep it small" into a number that can fail a build. Know what you would measure, at which compression level, and what happens the day the budget breaks.

on this pageshow

questions

5

A frontend team writes its JavaScript bundle budget as "150 KB". Which measurement should that number refer to — the file on disk or the bytes on the wire — and why does the budget also have to name the compression algorithm?

level: juniorimportance: should knowfreq 55%

answer

  1. the wire, not the disk
  2. two algorithms, two numbers
  3. quality level changes the result
  4. state the unit and the encoding

basics

~20 s

A transfer budget should be measured on the compressed bytes actually sent, not the file on disk, and it must name the algorithm: brotli output for the same JavaScript is typically noticeably smaller than gzip, so "150 KB" is ambiguous otherwise.

solid answer

~50 s

A bundle budget is a proxy for what the user pays to download, so it should be measured on the **compressed** bytes that leave the server, not on the minified file sitting in `dist/`. Those are very different numbers — minified JavaScript commonly compresses three to four times over, so a 480 KB file can be ~120 KB on the wire. Because gzip and brotli produce different sizes for the same input (brotli is usually the smaller of the two for text), and because compression quality levels also change the result, a budget that just says "150 KB" is unfalsifiable. Write it as "150 KB brotli, quality 11" or "150 KB gzip -9", measure it the same way in CI every time, and confirm separately that production actually serves that encoding — a budget measured with brotli against a server that only sends gzip is measuring a file nobody downloads.

code

bash · 3 lines
bash
wc -c < dist/main.js
gzip -9 -c dist/main.js | wc -c
brotli -q 11 -c dist/main.js | wc -c

go deeper

for a junior

Know that a bundle has a raw size and a smaller compressed size, and that a network budget refers to the compressed one. Be able to say plainly that gzip and brotli give different numbers.

for a middle

Explain the pipeline: minify, then compress, then the browser decompresses before parsing. Be ready to say roughly how much minified JavaScript compresses and why brotli usually beats gzip on text.

for a senior

Show that you verify the budget against what production actually serves — the response encoding and transferred size — and that you know build-time brotli 11 flatters a setup where the edge compresses on the fly at a lower quality.

for a principal

Own the definition itself: one measurement method, one unit convention, one place the number lives, applied identically in CI and in reports, so size claims across teams are comparable rather than each team quoting its favourite tool's output.

## Three different sizes of the same bundle Every built JavaScript asset has at least three sizes, and a budget is meaningless until you say which one it constrains. 1. **Raw / on-disk size** — the minified file the bundler wrote. This is the number a file listing shows and the number most bundlers print in their build summary. 2. **Transfer size** — the bytes that actually cross the network after the server applies `Content-Encoding` (gzip, brotli, or zstd where supported). For text assets this is typically a third to a quarter of the raw size. 3. **Decompressed size in the browser** — the browser inflates the response back to the raw bytes before parsing it. This one equals the raw size and is what the JavaScript engine has to parse and compile. A *transfer* budget is about the network: how many bytes a user on a slow connection must pull before the page can work. That is the compressed number. ## Why the algorithm has to be named Compression is not one thing. Gzip (DEFLATE) and brotli are different algorithms with different results on the same input, and brotli additionally ships a built-in dictionary tuned for web text, so it usually wins on HTML/CSS/JS. For a typical bundle, brotli output lands meaningfully below gzip output — enough that a bundle can be inside budget under one and outside it under the other. Quality level matters too. Brotli exposes levels 0–11; level 11 is slow and is what you use when compressing **once** at build time, while a CDN or origin doing **on-the-fly** compression for every response usually runs a much lower level to keep CPU down. So a local `brotli -q 11` measurement can be optimistic relative to what production really sends. Either pre-compress at build time and ship the `.br` files, or measure at the quality your edge actually uses. ```bash # same file, three numbers wc -c < dist/main.js # raw, minified gzip -9 -c dist/main.js | wc -c # gzip transfer size brotli -q 11 -c dist/main.js | wc -c ``` ## Minification is a separate step A common muddle is treating minification and compression as the same thing. Minification is a *source transform*: renaming locals, dropping whitespace and comments, folding constants. It happens once, in the build, and its output is still valid JavaScript. Compression is a *byte encoding* applied to the response and undone by the browser before parsing. They compose — minify first, then compress — and minification is still worth doing even though compressed sizes converge somewhat, because the decompressed bytes are what gets parsed. ## Writing a budget that can actually fail A usable budget statement pins down four things: - **The artifact**: which file or which group of files (an entry point, a route's initial payload, "all JS", "all fonts"). - **The measurement**: compressed transfer size, and with which algorithm and quality. - **The number and its unit**: be explicit about KB (1000 bytes) versus KiB (1024) — tools differ, and the gap is about 2.4%, which is inside the range of a real regression. - **The consequence**: warn, or fail the build. Then measure the same way every time. The most common way a budget quietly stops working is that the number in the wiki was measured with one tool and the check in CI uses another, so the two drift and nobody trusts either. ## Verifying the number is real The last step people skip: check what production serves. Load the asset and look at the response's `Content-Encoding` and the transferred size the network panel reports. If the origin or CDN is not compressing that asset — a common miss for files served from an object store with the wrong content type, or for already-`.gz`-named files — your careful brotli budget describes a file that no user ever receives. The budget is a claim about user experience, so it should be anchored to something you can observe on the live site, not only to a number computed in the build.

  • Your CI budget check passes with brotli quality 11, but real users seem to download more than that. What would you check first?
    Whether production actually serves brotli for that asset, and at what quality. Look at the response's `Content-Encoding` and the reported transfer size on the live site. On-the-fly compression at the edge runs a much lower quality than 11, and a misconfigured origin may serve the asset uncompressed entirely — in which case the user pays the raw size.
  • If compression already shrinks JavaScript so much, is minification still worth the build time?
    Yes. Compression is undone before parsing, so the browser still parses every minified-away byte otherwise. Minification cuts the decompressed bytes the engine must tokenise and compile, and it also enables dead-code removal that compression cannot do, since compression only re-encodes bytes and never understands the program.

Quoting a bundle's on-disk size is like quoting a suitcase's weight before you vacuum-pack it: the airline charges for what actually arrives at the counter.

saying these in an interview costs you the question

  • Quotes the on-disk size as the bundle's cost to users
  • Assumes gzip and brotli give the same number
  • Treats minification and compression as one step
  • Measures locally at brotli 11 while the CDN compresses on the fly
  • Mixes KB and KiB when comparing against the budget

context

open as a page

Why is a bundle budget expressed only in compressed transfer bytes an incomplete control on the cost of JavaScript, and what second number would you budget alongside it?

level: middleimportance: should knowfreq 42%

basics

~20 s

Compressed bytes only measure download time. Parse, compile, execute and memory cost scale with the decompressed bytes, which can be many times larger and vary wildly by content — so budget uncompressed size, or estimated execution time, alongside transfer size.

open as a page

An app has one budget for "total JavaScript" and it has been green for months, yet the checkout route's payload has doubled. How should byte budgets be scoped so a regression like that fails the build instead of hiding?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Budget each route's initial payload — its entry chunk plus the shared chunks that route loads — instead of the sum of all emitted JavaScript. An app-wide total grows with every route added, absorbs offsetting changes, and hides one route doubling.

open as a page

You inherit a frontend app whose main bundle is already several times larger than any budget you would sensibly set. How do you introduce bundle-size budgets without turning the build red on day one?

level: seniorimportance: nice to knowfreq 36%

basics

~20 s

Set the initial baseline at today's measured size plus a small tolerance, so the check fails only on regression, then ratchet it downward automatically whenever a build comes in under. Keep a separate, user-derived target as the destination the ratchet is heading toward.

open as a page

A build fails because a genuinely needed feature pushes a route's JavaScript over its size budget. What policy around bundle budgets keeps that failure useful, rather than something teams routinely override?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Treat the breach as a forced decision with named options — pay for the feature by cutting or deferring something else, ship it lighter, or raise the budget deliberately with an owner and a recorded reason — and never let the raise happen silently in the same change that caused it.

open as a page