Five gates, not one budget
A page weight budget written as a single number is a budget you can pass by accident. Ours is 300 KB on a first uncached view — and if that were the whole rule, a page could ship 250 KB of JavaScript, clear the total, and still be the slowest thing on the site.
So the number is split. Five classes, each with its own gate, each independent:
| Class | Gate |
|---|---|
| HTML | 15 KB |
| CSS | 20 KB |
| Fonts | 75 KB |
| JS | 15 KB |
| Images | 175 KB |
A pass on the total does not excuse a blown class. That is the entire point.
The images gate is the one that looks generous and is not, because of how it
counts. It bills every candidate the build emits, not the one your browser
picks — so a conventional two-format, four-width <Picture> bills eight files
for one screenshot. That accounting is why rasters here are AVIF only on a
two-rung ladder (root ADR-0013), and the specimen below is a real measurement
of it rather than a promise:
Generated by yarn build:seed-image, at one content column and its retina
rung — the size a real screenshot lands at, so the gate reports a real number.
Why the classes differ
The gates are not one number divided five ways, because the classes do not cost the same. Bytes of JavaScript are parsed and executed on the main thread; bytes of image are decoded off it and can be deferred entirely. Treating them as interchangeable is how a budget gets met while the page gets worse.
The JS gate is the sharp one
15 KB is small enough that it forces a question rather than a diet. Every
interactive piece of this site is an island — a .svelte component under
components/islands/, hydrated by one of three directives and no others:
// The whole list. `client:visible` is the default and hydrates when the
// component scrolls into view; `client:load` is for something above the fold
// that is visibly wrong until it hydrates; `client:media` needs a comment
// quoting the breakpoint it mirrors.
export const DIRECTIVES = ['client:visible', 'client:load', 'client:media'];
client:idle is banned outright: it hydrates offscreen work regardless of
whether anyone will ever see it, which is exactly the cost the gate exists to
refuse. client:only is banned too, for a different reason — it cannot be
correct with JavaScript off, and every budget here is measured against a page
that is.
What that leaves out
Not everything survives a 15 KB gate, and the honest consequence is that some features do not get built the way the obvious library would build them.
What the gate does not measure
Transferred bytes are a proxy. They correlate with what a visitor actually waits for, but they are not it — a 5 KB script that blocks on a network round trip is worse than a 40 KB image below the fold.
Budgets are a forcing function, not a measurement. The measurement is Core Web Vitals, and it is reported rather than gated.
The gates catch the regression you did not notice. The vitals tell you whether the page is good. They are different jobs, and collapsing them into one number does neither well.