Sky.Spa — production web (exploration + measured evidence)
Status: exploration (v2). Sky.Spa v1 ships desktop/mobile-embed weight (Go→wasm, ~2.4 MB gzip for the Todos app). This document is the evidence-based plan for a web-viable bundle. Every size here is measured on this machine.
The goal
A public-web-viable client bundle. Reference points: Elm ~30 KB gzip, a React app's runtime ~40–130 KB gzip. Sky.Spa v1's Todos client is ~2.4 MB gzip — fine for logged-in/internal tools + installable PWAs (load-once, cached), too heavy for cold public web.
The blocker (confirmed against the code)
The wasm client core is reflection-native, in three places, all compiled into
the client (none //go:build !js):
- Dispatch —
runtime-go/rt/live_core.go:2425,2436,2516:sky_call/sky_call2invoke the app'supdate/viewviareflect.Value.Call. - Codec —
runtime-go/rt/codec_auto.go: 85 reflect sites (Codec.auto, used to decode the client'sdata). - ADT —
runtime-go/rt/adt_shape.go,rt.gokernels.
TinyGo — the only lever that shrinks wasm ~10–20× — implements neither
reflect.Value.Call nor reflect.MakeFunc. So it cannot compile this core.
The half-built escape hatch does not help: perMsgTypedDispatch
(msg_dispatch.go) is Stage-5 scaffolding only — it always falls through to
reflect (LookupMsgUpdate is empty in every real binary; codegen emits no
RegisterMsgUpdate; the Stage-6 "thread the ADT name from the call site" step
never landed).
Measured evidence (this machine, 2026-08-22)
The Phase-1 spike (docs/skyspa/spike/main.go) is a reflection-free Go→wasm
TEA+DOM counter — a faithful stand-in for what a de-reflected Sky.Spa client
would be. Compiled both ways:
| Toolchain | raw | gzip |
|---|---|---|
standard Go (GOOS=js) | 1,996,836 B (1.90 MB) | 592,397 B (579 KB) |
TinyGo 0.41.1 (-target wasm) | 195,970 B (191 KB) | 66,999 B (65 KB) |
~9× smaller, 65 KB gzip — web-viable. And the current v1 real-app figure for contrast: the Todos client (reflect-heavy, standard Go) is ~2.4 MB gzip.
A trivial counter under-measures, so a todos-SCALE reflection-free surrogate
was also built (scratchpad/tinygo-complex — a {ui,data} model, switch-based
typed dispatch over 12 Msg branches, a real list+form+filter+router view with
per-item rows, a hand-written typed JSON codec, syscall/js fetch):
| todos-scale, reflection-free | raw | gzip |
|---|---|---|
| standard Go→wasm | 2,077,533 B | 614,485 B (600 KB) |
| TinyGo 0.41.1 | 301,206 B | 105,020 B (103 KB) |
103 KB gzip for a realistic app — decisively web-viable. Against the current reflect-heavy Todos client (~2.4 MB gzip), Path A is a ~23× reduction. This is the number that matters: the de-reflected design, at real app complexity, lands in production-web territory.
So the two facts that decide the plan: (1) TinyGo turns a reflection-free client into a web-viable bundle; (2) the current client is reflection-heavy, so TinyGo is blocked until it is de-reflected.
The three paths
Path A — de-reflect the client core, compile with TinyGo (recommended)
Make the client core reflection-free, then TinyGo it. Work:
- Dispatch — finish the
perMsgTypedDispatchStage 6: a typed dispatch table keyed on the Msg ADT (thread the ADT name from the call site), soupdate/vieware invoked withoutreflect.Value.Call. Scaffolding exists. - Codec — the client decodes
datawithCodec.auto(reflection). Replace with the typed/generated codecs the app already declares (the Todos app hand-writestodoCodecetc. inShared.sky— those are reflection-free already; the reflection is inCodec.auto's auto-derivation). A codegen or a reflection-freeCodec.autofor the client target. - ADT shape — a reflection-free ADT representation for the client.
- Isolate — do all of this in a separate
rt_spaclient runtime (or a//go:build js && spavariant), so Sky.Live's server reflect path is untouched and unregressed. - Verify TinyGo covers the rest — TinyGo's stdlib is narrower than Go's (not only reflect). The whole reflection-free client core must be checked against TinyGo's supported packages, not just the spike.
- Payoff (measured surrogate): ~65 KB gzip (counter) and ~103 KB gzip (todos-scale, reflection-free surrogate) — both under TinyGo, both web-viable. The true figure is the real de-reflected Sky-emitted client, measured after the rewrite (the surrogate hand-writes what codegen would emit), but the two data points bracket it firmly in web-viable range.
- Cost: a bounded runtime rewrite (dispatch + codec + ADT), isolatable to a client runtime. Multi-week. Reuses the entire Go toolchain + the existing IR.
- Risk: TinyGo stdlib gaps beyond reflect; the codec de-reflection is the largest single piece.
Path B — a Sky→JS backend (long-horizon ideal)
A new codegen target emitting JavaScript from the existing typed IR (like Elm→JS). Smallest bundles (~30 KB), best web perf, and JS's dynamism dissolves the reflect problem entirely.
- Payoff: the best possible web story.
- Cost: enormous — a whole new emit backend (codegen currently emits Go).
Multi-quarter. Reuses
hir/tybut not the Go runtime.
Path C — ship Go→wasm for web now, mitigated (interim)
Keep standard Go→wasm; mitigate the ~2.4 MB: brotli (smaller than gzip), streaming compilation, lazy-load/split, aggressive caching.
- Payoff: zero rewrite; usable today for logged-in apps, internal tools, and installable PWAs (the bundle is fetched once and cached).
- Cost: none. Not suitable for cold public-web first paint.
Recommendation
- Now: Path C is already what v1 is — document it as the supported web story for logged-in/PWA use, add brotli to the static serving.
- The web bet: Path A. The measured 9× (579 KB → 65 KB) confirms de-reflection + TinyGo reaches web-viable, it reuses the whole Go toolchain, and it can be isolated to a client runtime so Sky.Live is untouched. The scope is a bounded, ordered rewrite (dispatch → codec → ADT), each piece independently verifiable, with a real-app bundle measured at the end.
- Long-horizon: Path B (Sky→JS) if/when a from-scratch web-native backend is worth a multi-quarter investment; Path A does not preclude it and buys the web story far sooner.
Honest caveats
- 65 KB is a counter and 103 KB is a hand-written todos-scale surrogate — the real Sky-emitted de-reflected client is unmeasured until the rewrite. Quote "~100 KB gzip, web-viable (surrogate-measured)", not a hard v1 number.
- TinyGo compatibility of the full reflection-free core (beyond the spike) is unverified — a required Path-A spike is: de-reflect the smallest real client (spa-counter) and TinyGo it end-to-end.
- The de-reflection touches the runtime; isolating it to a client-only runtime is a hard requirement so Sky.Live's (correct, reflection-based, server-side) path does not regress.