00 — Rationale
Status: draft
The problem
A Soli application today is fast on the server and expensive on the client. The server side is measured and good: ~95 µs of CPU to render a 50-row HTML template, 95 MB PSS at idle for the whole process against 643 MB for Django. The client side is a web browser.
To display that 50-row table, the browser must:
- parse HTML with an error-tolerant parser that cannot fail, only guess;
- build an untyped, dynamically-shaped DOM;
- match every CSS selector against every element, and resolve the cascade;
- compute inherited and specified values into computed values, per element;
- reflow a tree whose constraints are discovered rather than declared;
- keep a JavaScript JIT resident, with its attack surface and its memory.
Steps 1, 3, 4 and 6 exist to support documents authored by strangers over thirty years. An application UI does not need them. A counter costs 300 MB of RAM and a busy core because of machinery it never asked for.
The move
Stop sending a document to be interpreted. Send an interface tree that is already resolved.
| Browser does at runtime | EUI does where |
|---|---|
| Parse markup | Server emits typed records; client validates, does not parse |
| Match selectors, resolve cascade | Server resolves; client does one array lookup |
| Compute inherited values | Server resolves |
| Reflow an untyped tree | Client lays out a typed tree with a fixed algorithm |
| JIT untrusted JavaScript | Client runs verified, metered bytecode — or nothing |
| Rasterise via a general engine | Client draws rounded rects and glyphs, one shader |
The client keeps only the parts that genuinely must be local: layout (it depends on the viewport and the user's font scale), text shaping, painting, and the handful of interactions that must not cost a round trip.
Three encoding ideas
Session atom table. Every repeated string — property names, labels, list
keys — is sent once as DefAtom(id, bytes) and referenced by a varint
thereafter. HTML re-emits <div class="…"> on every row of a table; EUI sends
the atom once for the whole session.
Computed style table. DefStyle(id, record) defines a fixed-layout 64-byte
record of computed style. A thousand table rows share three style ids. There
is no cascade on the client because there is nothing to cascade.
Node arena. The tree decodes into one contiguous Vec<Node> with u32
child/sibling indices. One allocation, cache-friendly traversal, no pointer
chasing, no per-node allocation.
What EUI deliberately refuses
These are not gaps to be filled later. They are the reason the client is small.
- No open-ended element vocabulary. The protocol can express exactly the
primitive node kinds in
03-widgets.md. Richer widgets are composed on the server from those primitives, which is why the widget catalogue can grow without shipping a new client. - No cascade, no selectors, no specificity, no
!important. - No arbitrary code from the network. No native code, no JIT, no
eval. Exactly two kinds of executable content reach the client, and each has a verifier of its own: bytecode, by07-bytecode.md, and — only where thescenecapability was granted — a WGSL module, by11-shaders.md. Both are total by construction: the bytecode's steps are bounded at run time by fuel, the shader's by its shape before it is ever compiled, because a fragment stage cannot be stopped once it is running. Neither can call the platform, allocate, keep state between runs, or reach the other. - No ambient authority. A capability not granted in the manifest has no code path in the runtime, not merely a failing check.
- No error recovery. A malformed frame ends the session. Tolerance is how parsers become attack surface.
- No layout escape hatches. No floats, no CSS
position, nocalc(). Layout is one specified algorithm so that every implementation agrees on the pixel. - No fingerprinting surface. No user-agent string, no font enumeration, no
canvas readback, no device identifier — and a
scenekeeps every one of those, by construction rather than by rule (08-security.md§8). What a scene does not close is the coarse timing channel awakeand a sound already give a server; 08 §8 says so rather than implying otherwise.
What "light and fast" means, concretely
Normative budgets live in 10-budgets.md and are enforced in
CI. The headline ones:
- 0 % CPU and zero wakeups at idle — an architectural consequence of an event-driven redraw with no continuous render loop, not a tuning parameter. Something that moves is not idle: a spinner, a playing picture and an animating scene each redraw at the cadence they asked for, and each does it without waking the part of the client that reads the server's bytes;
- < 25 MB RSS for a 200-node application;
- 4.6 KB on the wire for a 50-row table against 14.4 KB of HTML — 3.1× overall, 5.3× on structure alone, measured rather than hoped. Compressed, the HTML is smaller (910 B to 1 491 B): its bulk is one tag repeated, which is what gzip removes, and EUI removed it already. First-paint bytes are not the claim;
- 22 bytes for a single-cell update.
If a change cannot hold these, the change is wrong, not the budget.
Rendered from spec/00-rationale.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