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.

  1. 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.
  2. 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.
  3. 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 ↗

SPACEMUSIC CORE (NATIVE / vvvv) ~11k channels the parameter tree coalesce 10k writes → 1 encode MessagePack / JSON transport Centrifugo / WS BROWSER A · Avalonia WASM decode + render in .NET .NET WASM decode Svelte render B · .NET data layer + web UI C · pure-JS Svelte decode + render in JS the decode lives here — that's the whole difference.

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 ↗

0 2,000 4,000 6,000 ms A Avalonia 6,082 ms ≈24× slower — the whole Avalonia + .NET runtime downloads and compiles before first paint B .NET layer 529 ms page paints in 108 ms, but first data waits on a lazy .NET-WASM boot C pure JS 250 ms 34 KB bundle, no .NET — the fastest to a usable frame

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:

OptionFirst coherent frameBundle / runtimeType fidelityVerdict
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:

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.

Figure 1 · Headless UI Decision · designed for A3 landscape print ← back to the document

Figure 1 · The wire path, and where the three options diverge

SPACEMUSIC CORE (NATIVE / vvvv) ~11k channels the parameter tree coalesce 10k writes → 1 encode MessagePack / JSON transport Centrifugo / WS BROWSER A · Avalonia WASM decode + render in .NET .NET WASM decode Svelte render B · .NET data layer + web UI C · pure-JS Svelte decode + render in JS the decode lives here — that's the whole difference.
Figure 2 · Headless UI Decision · designed for A3 landscape print ← back to the document

Figure 2 · Time to first coherent frame — lower is better

0 2,000 4,000 6,000 ms A Avalonia 6,082 ms ≈24× slower — the whole Avalonia + .NET runtime downloads and compiles before first paint B .NET layer 529 ms page paints in 108 ms, but first data waits on a lazy .NET-WASM boot C pure JS 250 ms 34 KB bundle, no .NET — the fastest to a usable frame