SproutlySquad.dev

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:

ClassGate
HTML15 KB
CSS20 KB
Fonts75 KB
JS15 KB
Images175 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:

A placeholder product screenshot: an app window with a sidebar of navigation items, a heading, a bar chart of twenty-four bars, and a seven-row data table, drawn in the SproutlySquad palette.

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.