Internal · SpaceMusic Engineering · Plan 036
Where the Headless UI Should Decode
Three ways to render SpaceMusic's controls in a browser. We built and measured all three — and the option with no .NET on the client reaches a usable frame about 24× faster than full Avalonia and about 2× faster than a .NET data layer.
The decision, and why it's expensive to get wrong
SpaceMusic's UI is moving to the browser. The engine stays native; the controls — sliders, dropdowns, the whole parameter tree — need to render and stay live on a remote screen, including a phone. The question this plan answers is narrow but load-bearing: where does the engine's typed data get turned into something a web page can show?
That sounds like an implementation detail. It isn't. The answer decides whether .NET ships to the client at all, how big the download is, and how long a phone sits on a blank screen before the UI is usable. Picking wrong means re-doing the UI — roughly a year of work. So before committing, we built working prototypes of the three real answers and measured them against the same realistic load.
Three places to decode
The options differ in one thing: how much .NET rides along to the browser, and therefore where the typed channel data gets decoded into renderable values.
- Option A — full Avalonia in the browser. The existing C# UI is compiled to WebAssembly and runs whole in the page. Highest fidelity, zero re-modelling — but the browser downloads and boots the entire Avalonia + .NET runtime before anything paints.
- Option B — a .NET data layer plus a web UI. A small .NET module in the browser keeps the C# types on the wire, decodes them in WebAssembly, and hands typed values across an interop boundary to a Svelte UI. Less .NET than A, but still a .NET runtime on the client.
- Option C — pure web, translate at the core. The SpaceMusic core translates the typed channel graph to JSON or MessagePack; a plain JavaScript/Svelte UI decodes it. No .NET on the client at all.
Fidelity runs A ≥ B > C: the further the C# types travel, the less the client has to re-model by convention. Cost runs the other way. The whole study is about pricing that trade.
How we measured it
A fair comparison needs every option doing the same work. We built a shared wire codec (MessagePack and JSON, identical message shapes), a workload generator that replays a realistic scene-load — about 11,000 channels coalesced into a single burst — and a measurement harness that records the user-felt metrics in the browser. All three prototypes render the same 250-component page from the same channel spec the generator pumps; only the decode path differs. That isolation is the point: any difference we see is the decode location, not the test.
Figure 1 · The wire path, and where the three options diverge Open full size · print A3 landscape ↗
Everything left of the dashed line is shared and option-independent. The coalescing step alone is the single biggest win in the study, and it applies to all three: a scene-load that would fire ~10,400 separate socket messages collapses to one. The interesting divergence is entirely to the right — how much runtime each option drags into the browser to turn bytes back into a slider.
What the numbers say
The metric that matters is time-to-first-coherent-frame (TTFCF): from the moment the UI asks for the scene to the moment all visible controls show their loaded values. It's what a user actually waits through. Same 250 components, same burst, measured in Chromium.
Figure 2 · Time to first coherent frame — lower is better Open full size · print A3 landscape ↗
A is not close. Its HTML shell loads in 38 ms; the remaining six seconds are the Avalonia and .NET runtime downloading, compiling, and laying out — a near-disqualifier for the mobile story, and only worse on a slow network. B is far better than A but still pays for the runtime it carries: its Svelte page paints as fast as C's, yet first data waits ~280 ms longer for the .NET-WASM layer to boot and marshal values across the interop boundary, plus a multi-megabyte runtime download. C carries nothing extra.
Putting the user-felt metrics next to the fidelity argument gives the decision matrix:
| Option | First coherent frame | Bundle / runtime | Type fidelity | Verdict |
|---|---|---|---|---|
| A Avalonia.Browser | 6,082 ms | Avalonia + .NET (MBs) | Highest, no re-modelling | Out for mobile |
| B .NET data layer | 529 ms | .NET runtime (MBs, lazy) | High through decode | Fidelity-only case |
| C pure-JS Svelte | 250 ms | 34 KB, no .NET | By convention | Lead |
One result cuts against intuition and is worth keeping: on localhost, JSON decoded faster than MessagePack on the client (the browser's native JSON.parse beats the JavaScript MessagePack decoder), even though JSON is ~1.5× more bytes. MessagePack's advantage is the wire, not client CPU — which means the encoding choice is a network question, to be settled under throttling, not a reason to keep .NET on the client.
What this doesn't settle yet
These are single-run, small-spec, localhost, headless numbers. The ranking is robust — a 24× and a 2× gap don't reverse — but the exact milliseconds are indicative, not final. Three things still have to land before this is a decision rather than a strong lean:
- The network environments. Throttled "Fast 3G" with a cold cache, and the real Centrifugo relay over TLS. We expect this to widen the gaps, not close them — the multi-megabyte runtime downloads A and B carry become latency-bound, and that's exactly where MessagePack's smaller wire starts to pay.
- Firefox. Mandatory; single-browser numbers under-state variance. The harness already flags which metrics are Chromium-only.
- B's actual case. The lean B we measured proves the cost but not the benefit — it doesn't yet decode the complex types (colors, vectors, nullable records) that are B's only reason to exist. Whether keeping C# types on the wire is even buildable for browser-WASM, and whether it justifies a 2× tax, is the open question for Option B.
Why it matters
"The cheapest client to ship is also the fastest one to use — and the burden of proof now sits on every megabyte of .NET we'd send to a phone."
For a UI that has to feel instant on a remote screen, the thing a user feels is cold-start and main-thread latency. On those, the option with no .NET on the client wins by a wide margin, and full Avalonia loses badly enough to drop for mobile. That doesn't make Option C free: it trades type-fidelity for client-simplicity, and it has to re-model a handful of awkward types — colorspaces, nullability, typed vectors — by explicit convention. But that's a bounded, one-time cost on a schema that's been stable for a decade, paid against a runtime tax that recurs on every page load.
So the lean is Option C, with Option B kept alive only if its fidelity case can be made to outweigh a measured ~2× tax — and with producer-side coalescing and MessagePack adopted regardless, because both help every option. The next measurements decide whether the lean becomes the recommendation.
Glossary
Terms used above, in plain language.
- TTFCF
- Time-to-first-coherent-frame. From asking for the scene to all visible controls showing their loaded values — the wait a user actually experiences.
- WASM
- WebAssembly. A compiled binary format browsers run; how .NET (and Avalonia) execute inside a web page.
- Avalonia
- The cross-platform C# UI framework SpaceMusic's desktop UI is built in; "Avalonia.Browser" is it compiled to WASM.
- Svelte
- The JavaScript UI framework used across the SpaceMusic server stack; the web UI for options B and C.
- Channel
- One live value in the engine — a slider position, a toggle, a dropdown selection. The UI binds to roughly 11,000 of them.
- Scene-load
- The burst when a project opens and every channel sends its current value at once — the dominant, most-felt moment.
- Coalescing
- Collapsing many writes to a channel within one tick into a single message — turns ~10,400 scene-load sends into one.
- MessagePack
- A compact binary encoding; ~1.5× smaller on the wire than JSON, but not faster to decode in a browser.
- Centrifugo
- The WebSocket relay (on our Hetzner server) that carries channel updates to remote clients.
- Category 7
- The plan's shorthand for the hard types — colors, vectors, matrices, nullable records — where option C must invent conventions and B's fidelity case lives.
Settled
C leads; A is out for mobile
All three built and measured. C reaches a usable frame in 250 ms vs A's 6,082 ms and B's 529 ms. Coalescing + MessagePack adopted regardless.
Next
Network + Firefox
Throttled cold-cache, the Hetzner relay, and Firefox. Expected to widen the gaps and let MessagePack's smaller wire pay off.
Open
B's fidelity case
Whether keeping C# types on the wire is buildable for browser-WASM at all, and whether it justifies the measured ~2× tax.