Budgets
Soli's benchmark document publishes the rows it loses. This one does the same: where a number comes from a run it says so, and where it is a target it says that instead. A budget that was never measured is a slogan.
Wire — measured
From crates/eui-proto/tests/size_budget.rs. Run it yourself:
cargo test -p eui-proto --test size_budget -- --nocapture
The subject is a 50-row, four-column invoice table: 256 nodes, keyed rows, six shared style records. The HTML side is the same table with the Tailwind classes such a table really carries, unindented — the favourable case for HTML.
| Bytes | |
|---|---|
| Cell and header text, identical both ways | 2 337 |
| EUI total | 4 619 |
| EUI with the status column interned | 4 209 |
| EUI, of which style records, once per session | 396 |
| EUI structure only | 2 282 |
| HTML total | 14 362 |
| HTML structure only | 12 025 |
Total 3.1×. Structure 5.3×. 8.9 bytes of structure per node.
The text is the data — neither side can compress it away — so the honest headline is the structural ratio.
Under Content-Encoding: gzip, HTML wins
| Raw | gzip -9 | |
|---|---|---|
| EUI | 4 619 | 1 491 |
| HTML | 14 362 | 910 |
That is not a typo and it is not close: compressed, the HTML is smaller. HTML's bulk is the same few tag names and class strings repeated fifty times, which is precisely what a compressor exists to remove; EUI has already removed that repetition itself, so what is left is dense and gzip finds little in it. A format that does its own deduplication competes badly against one that hands the job to zlib — and in the real world the HTML is gzipped.
So the first-paint byte comparison belongs to HTML, and anything that claims otherwise is quoting the raw column and should be corrected. Two things are unaffected, and they are the ones worth making the argument on:
- Updates. A single cell is 22 B and re-sorting fifty rows is 201 B. Compression gives HTML no answer to that, because HTML has no update: the page is sent again, and 910 B compressed is still forty times 22 B.
- Work. Compression makes the receiving machine's job larger — it
inflates, and then still does the tolerant parse, the selector matching,
the cascade, the layout of a tree whose types it inferred, and the script.
That is what
00-rationale.mdand the README lead with, and it is the claim that was always load-bearing.
Measured by what_the_ratio_is in
crates/eui-proto/tests/size_budget.rs, which fails if either half of this
reverses. The regression budget is set at 4×, below the
measured 5.3×, so a real regression trips it and ordinary drift does not.
| Operation | Measured | Budget |
|---|---|---|
| Single-cell update | 22 B | under 40 B |
| Reversing 50 keyed rows | 201 B | under 300 B |
| The same reversal by re-mounting | 4 619 B | — |
That last pair is the argument for MoveChild.
Runtime — targets
Not yet measurable: the client does not exist. These are what
cargo run --release -p xtask -- bench will enforce.
| Budget | |
|---|---|
| Launch to first pixel | under 80 ms |
| Idle, 200 nodes | 0 % CPU, zero wakeups |
| Idle RSS, 200 nodes | under 25 MB |
| 10 000-row virtualised table, RSS | under 45 MB |
| The same table, scrolling | 60 fps, under 2 ms CPU per frame |
| Client binary, stripped, two variable fonts included | under 12 MB |
Measured on 2026-09-06: 12.13 MB with the default features, 9.81 MB
without accessibility (--no-default-features). The default build misses the
budget by one per cent; the 2.3 MB is AccessKit and the AT-SPI bus client on
Linux, and it stays in — an accessible client is the one that ships. That is
where the next size work goes.
Zero wakeups is architectural, not a setting: winit runs in
ControlFlow::Wait and the client redraws only when something asked it to.
There is no render loop in the code to leave running by accident.
Decoder — targets
| Budget | |
|---|---|
| Decode a 4 KB batch | under 50 µs |
| Allocations per batch | at most 3 |
| Peak decode memory | at most twice the frame size |
What none of this claims
Bytes on the wire were never the main prize. A 3.1× reduction matters on a slow link, but the reason EUI exists is what the client does not do with those bytes: no tolerant parse, no selector matching, no cascade resolution, no reflow of an untyped tree, no JIT. Those are the runtime numbers, and they are the ones worth judging the project on once there is a client to measure.
Rendered from doc/docs/eui/budgets.md in the repository. The
normative text is spec/; where it and a prose page
disagree the spec wins, and that is a bug worth reporting.
Edit this page