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?
answer
- nobody downloads the total
- sums hide offsetting moves
- count shared chunks per route
- the vendor chunk is everyone's tax
- initial strict, deferred loose
basics
~20 sBudget 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.
solid answer
~50 sA single "total JS" number is the wrong unit because no user downloads the total. Users download one route's initial payload, so that is what the budget should constrain: the entry chunk plus the shared and vendor chunks that route pulls in on first load, counted per route. Two things go wrong with a global total. It nets out — one route shrinking masks another doubling — and it has no fixed meaning, since adding a lazily-loaded admin section legitimately grows the total while costing ordinary users nothing. The other trap is scoping a route budget to only its own chunk: then the cheapest way to pass is to push code into the shared vendor chunk, which makes the number look good and makes every route slower. So count shared chunks against each route that loads them, budget the busiest routes explicitly, and keep the app-wide total as a reported trend rather than the gate.
go deeper
Know that a budget should describe what one page actually downloads, not the size of the whole build output, and that a route usually loads several chunks together.
Explain the two failure modes concretely: a sum hides offsetting changes, and scoping to a route's own chunk lets code be relocated into a shared chunk to game the number.
Show you would define the budget table — which routes get explicit numbers, one blanket per-route ceiling for the rest, shared chunks counted against each consumer, initial versus deferred separated — and say what a breach tells you to investigate.
Own the incentive design: budgets are read as instructions, so choose a scoping that makes the healthy move (deferring weight, keeping shared chunks lean) also the move that makes the numbers better.
## The unit of a budget is what one user downloads A performance budget is an assertion about a user's experience. Users do not experience "all the JavaScript this app can emit"; they land on one URL and download whatever that URL needs before it works. The scope of a budget should match that: **the initial payload of a route or entry point**. For a code-split app that payload is normally the sum of: - the entry/runtime chunk, - the framework and vendor chunks that entry loads, - any shared/common chunk the route depends on, - the route's own chunk, - plus render-blocking CSS if you budget that too. Anything downloaded later, on interaction or on navigation to another route, belongs to a different budget — usually a looser one, because deferred bytes cost less. ## Why one global total fails **It nets out.** A total is a sum, and sums absorb offsetting movements. The marketing route dropping 40 KB in the same release that checkout gains 40 KB leaves the number flat and the build green, while the revenue-critical path got measurably worse. **It has no stable meaning.** Adding a lazily-loaded admin area or a rarely-visited settings screen increases the total without costing any ordinary user a byte. Under a global budget those additions look like regressions, so teams raise the number to accommodate them — and each raise silently grants headroom to the routes that matter. Within a few releases the budget tracks the codebase rather than constraining it. **It cannot be aimed.** When a total budget breaks, nobody owns it. When a route budget breaks, the team that changed that route owns it, and the fix is scoped to what they just did. ## The shared-chunk trap The obvious correction — budget each route chunk on its own — introduces a worse failure. If only `routes/checkout.[hash].js` is counted, the cheapest way to get under the number is to move code out of it into a chunk that is not counted: the vendor bundle, the common chunk, the entry. The measured number improves. The user's download gets bigger, because that code now loads for every route rather than one. So the rule is: **count every chunk a route causes to be downloaded on first load, against that route.** Shared bytes get counted multiple times across the budget table, once for each route that needs them, and that is correct — it reflects that a byte in the vendor chunk is a byte everyone pays. It also creates the right incentive: moving code into a shared chunk now makes several budgets worse at once, which is exactly the pressure you want when someone proposes it. A useful diagnostic that falls out of this: if every route's initial payload is dominated by the same shared chunk, the shared chunk is the product's real budget problem, and no route-level trimming will help. ## Choosing which routes to budget Budgeting every route is unnecessary and unmaintainable. Pick: - **the entry / landing path**, since it sets first impressions and is usually the highest-traffic; - **the critical business flow** — checkout, signup, search; - **the current worst offender**, so it is visibly capped while being worked on. For everything else, a per-route ceiling that no route may exceed works better than individual numbers: one rule, applied to all routes, that fails whichever route breaks it. Then keep the app-wide total as a *reported* trend line — it is genuinely interesting for tracking overall bloat — but not as the gate. ## Related scoping choices Two more dimensions are worth pinning down when you define the budget table: - **By resource type.** Separate budgets for scripts, stylesheets, images and fonts fail more informatively than one blended byte count, because the remedies are completely different. - **Initial versus total.** Budget initial load strictly and deferred/on-interaction bytes loosely. This is what keeps a budget from punishing the very deferral you want teams to do — otherwise the incentive is to avoid splitting, since anything counted the same whether it loads now or later is cheaper to just ship eagerly. Get those two right and the budget stops being a number people work around and starts being a description of what the product promises its users on each screen.
- If shared chunks are counted against every route that loads them, aren't those bytes double-counted?They are counted repeatedly on purpose. The budget models per-user cost, not disk usage, and a byte in the vendor chunk is paid by every route that needs it. Counting it once would make moving code into the shared chunk look free, which is the exact incentive you are trying to remove.
- How do you keep a per-route budget table from becoming unmaintainable as routes multiply?Do not enumerate every route. Set explicit numbers for a handful that matter — landing page, the critical flow, the current worst case — and apply one blanket ceiling to all remaining routes, so any route that crosses it fails. Review the explicit entries when the product's critical paths change, not on every release.
- Should deferred, on-interaction chunks count against the same budget as the initial payload?No — budget them separately and more loosely. If deferred bytes cost the same as initial bytes, teams have no incentive to defer anything, and splitting work looks like wasted effort. A strict initial budget plus a generous deferred budget rewards moving weight off the critical path while still capping total growth.
saying these in an interview costs you the question
- Gates on one app-wide total JavaScript number
- Budgets only a route's own chunk, ignoring shared chunks
- Raises the total budget whenever a lazy route is added
- Assumes offsetting changes across routes are harmless
- Applies the same budget to eager and deferred chunks