skip to content

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%

answer

  1. download is only the first stage
  2. the engine sees decompressed bytes
  3. ratios are not constant
  4. repetitive data compresses ten to one
  5. pair bytes with an execute-time check

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.

solid answer

~40 s

Transfer size answers one question: how long the download takes. But the browser decompresses the response before doing anything with it, so parse, compile, execution and heap cost all scale with the **uncompressed** bytes. Compression ratios are not constant — highly repetitive generated output, a big locale or icon table, or a duplicated polyfill can compress ten to one, so a chunk can grow hundreds of kilobytes of real main-thread work while barely moving a brotli budget. On mid-tier mobile hardware that main-thread work is usually the part users feel, not the download. So I pair a transfer budget with an uncompressed-size budget on the same artifacts, and where the tooling allows it, a budget on estimated download-plus-execute time on throttled hardware, which is the number closest to what the user actually experiences.

go deeper

for a junior

Know that the browser decompresses a bundle before running it, so the compressed number is not the amount of code the engine handles. Be able to name download, parse and execute as separate costs.

for a middle

Explain why compression ratios vary and give a concrete case — repetitive generated data or a duplicated dependency — where transfer size stays flat while parse work grows. Name uncompressed size as the companion budget.

for a senior

Argue from the device: on low-end mobile, main-thread scripting usually dominates download, so demonstrate that you gate on decompressed bytes or an execute-time estimate rather than trusting a green transfer budget.

for a principal

Decide which pair of numbers the organisation gates on and on which device profile they are defined, so that a passing build means something about real users rather than about the fastest machine in the fleet.

## What the compressed number does and does not cover Shipping JavaScript costs the user in stages: fetch it, decompress it, parse it, compile it, execute it, and hold the resulting objects in memory. A compressed-transfer budget prices the **first** stage only. Everything after decompression is priced in the *uncompressed* bytes, because that is what the engine receives. That would be harmless if the compression ratio were constant — a transfer budget would then be a fixed-scale proxy for everything else. It is not constant, and the cases where it diverges are exactly the cases that hurt. ## Where the ratio breaks down Compression exploits repetition. So the artifacts that compress best are the ones with the most repeated structure: - **Generated data tables** — locale data, timezone tables, country lists, icon path data. Highly repetitive, often ten to one or better. - **Machine-generated code** — API clients generated from a schema, huge `switch` mappings, repeated wrapper boilerplate. - **Duplicated dependencies** — two versions of the same library in one bundle compress almost as well as one, because the second copy is nearly a repeat of the first. Transfer size barely notices; parse and execute pay twice. - **Polyfills and transpiler helpers** repeated across chunks. A change that adds 600 KB of such content might add only 15–40 KB compressed. A transfer-only budget waves it through, and the main thread absorbs several hundred milliseconds of extra parse and compile on a mid-tier phone. The reverse case exists too and is worth knowing: already-compressed content embedded in a bundle — a base64 image, an inlined font, an encrypted blob — barely compresses at all. It blows a transfer budget while costing almost nothing to parse, because it is a string literal, not code. ## The second number The simplest addition is an **uncompressed-size budget** on the same artifacts you already track. It is free to compute, deterministic, and it catches every case above. Track both numbers side by side; the ratio between them is itself a useful signal — a chunk whose ratio suddenly improves is usually a chunk that just absorbed something repetitive. The more faithful addition is a **time** budget: estimated download plus execute on a defined slow device and network profile. Some size-checking tools can run the bundle in a headless browser under throttling and report that number, and lab tooling can report main-thread scripting time for a page load. Time is the metric closest to the user, but it is noisier and slower to compute than bytes, which is why bytes remain the everyday gate and time is checked less often. ## Why the main thread is usually the binding constraint Bandwidth has improved far faster than single-core CPU performance on the low end of the phone market. A 200 KB compressed bundle downloads quickly on a decent 4G connection but can still take a long time to parse, compile and execute on a budget device — and unlike the download, that work happens on the main thread, blocking input handling and rendering. Two chunks of identical transfer size can therefore have very different user impact depending on how much of them is *code that runs* versus *data that sits*. This is also why "is it in the bundle" is not the same question as "does it execute". Code that is bundled but never called still costs download, decompression and parse, but not execution. Code inside a module with top-level work costs all of it at import time. When a budget breaks, knowing which kind of bytes arrived tells you whether to expect a network regression or a main-thread one. ## Putting it together A workable pair of budgets for an entry point looks like: - transfer size, compressed with a named algorithm — the network cost; - uncompressed size — the proxy for parse, compile and memory; - optionally, estimated execute time on a slow-device profile — the sanity check that ties the byte numbers back to something a user feels. Breaching only the first is a delivery problem: check compression, caching and what is loaded eagerly. Breaching only the second usually means something bulky and repetitive got added, or a dependency got duplicated. Breaching both is simply more code. The two numbers together tell you which conversation to have, which a single number never can.

  • You add a dependency and the compressed size barely moves while the uncompressed size jumps sharply. What do you suspect?
    Something highly repetitive arrived — a generated data table, a large icon or locale set, or a second copy of a library already in the bundle. Duplicates compress away almost entirely because the second copy repeats the first, yet the engine still parses and evaluates both. Confirm by comparing per-module uncompressed contributions before and after.
  • Is there a case where the compressed budget breaks but main-thread cost is essentially unchanged?
    Yes — inlining already-compressed content such as a base64 image, an embedded font, or a binary blob. It hardly compresses further, so transfer size jumps, but to the engine it is one long string literal: cheap to parse and never executed. The fix is a delivery fix, not a code-size fix.

saying these in an interview costs you the question

  • Assumes compressed size scales linearly with main-thread cost
  • Thinks the browser parses the compressed bytes directly
  • Ignores duplicate dependencies because they compress away
  • Treats download time as the dominant cost on mobile
  • Budgets only the total and never the uncompressed figure

context