Part 13 of the Fitz series. The frontend chapter opens: Fitz compiles
.fitzvcomponents to WebAssembly, and this is the dev loop that makes editing them feel instant — the same "save and see it" experience you get from Vite, on a language that compiles to a native binary.
The setup: a compiled language with a frontend
Fitz is a compiled language — HTTP, async, Postgres, JWT all live in the syntax and it emits a native binary via Rust. The frontend story is a single-file component format, .fitzv (state + events + <template>, à la Vue/Svelte), that compiles to WebAssembly:
fitz build --bin web --target wasm-client # → target/wasm/web/{web.js, web_bg.wasm}
No npm install, no bundler config, no external framework — the component becomes a self-contained WASM bundle (the counter demo is 11.4 KB gzipped).
Here's the reflex objection: compiled means slow feedback. Edit, wait for a full compile, refresh the browser by hand. That's the opposite of what a frontend loop should feel like. So Fitz has fitz dev.
The loop
Point fitz dev at a wasm-client bin and it stops being a compiler and starts being a dev server:
fitz dev # serves on http://127.0.0.1:1234/
What it does:
-
Incremental rebuild with
wasm-pack --dev(nowasm-opt), reusing a stable crate so cargo's cache stays warm — the first build compiles the deps, every save after that is ~1-2 seconds. -
A dev server that serves your project root like
python -m http.server: yourindex.html, your CSS, the bundle intarget/wasm/<bin>/. Noindex.html? It generates a minimal one at the mount point. -
Browser auto-refresh over a WebSocket: save a
.fitzv/.fitz/fitz.tomland the page reloads itself. No manual F5.
Save, and ~2 seconds later the browser shows the change. On a compiled language.
The detail that matters: state survives the reload
Most hot-reload stories lose your state on a full reload — you were three clicks into a counter, you edit the template, and you're back at zero. fitz dev snapshots the component's live state before the reload and restores it after:
component App {
state {
count: Int = 0
items: List<Str> = []
}
...
}
Bump count to 7, type two items into the list, then edit the <h1> above them. The reload lands and count is still 7, the list still has its two items. Primitives, lists, maps, nullables, custom nominal types — all survive (serialized to JSON into sessionStorage, restored on boot). You keep editing against the exact state you were testing.
(Honest caveat: a preserved state field wins over a changed default in the template — the state is what's preserved. Change a default and reset to see it.)
The manifest re-resolves live too
This is the piece that just shipped. Editing a .fitzv was always picked up (the entry is re-read every build) — but the manifest wasn't. Now it is: save fitz.toml and fitz dev re-resolves it without restarting.
-
Repoint
[bin].mainto a different component → next rebuild uses the new entry. -
Add a
[dependencies]— pull in a companion-UI component (from fitz_liveviews.ui.Badge import Badge) — → resolved and available on the next build. -
Edit
[flags]→ picked up live.
And a fitz.toml you broke mid-edit doesn't kill the loop: it prints the parse error and keeps serving the previous bundle, recovering on your next valid save.
↻ change in fitz.toml — rebuilding ...
↻ fitz.toml re-resolved
✓ reloaded browser
Fullstack: two bins, two terminals
A real app is a backend plus a frontend. In Fitz that's two bins in one manifest — a native server and a wasm-client web — wired with @rpc (server functions callable from the component as if they were local). You dev each half in its own terminal:
# Terminal 1 — the backend
fitz run --bin server
# Terminal 2 — the frontend, hot reload
fitz dev --bin web
--bin <name> selects the bin in a multi-bin project; it lands on fitz run and fitz build too.
Why this is different
Live-reload loops aren't new — the point is what it takes to get one:
-
Vite / webpack HMR give a great loop, but they're the JavaScript ecosystem: a bundler, a
node_modules, a framework runtime. Fitz's loop is one CLI over a language that compiles to WASM — no npm at all. -
cargo watchrebuilds and restarts, but there's no browser reload and no state preservation — you refresh by hand and start over. -
Rust WASM frameworks (Dioxus, Leptos) have hot-reload stories, but you're adopting a framework + its macro DSL + its toolchain. Here the component is the language, and
fitz devships with the compiler.
The combination — a compiled-to-WASM language, incremental rebuilds, browser auto-refresh, state that survives the reload, and a manifest that re-resolves live — is the part no single tool in the neighborhood gives you out of one binary.
The honest edges (MVP)
- It recompiles the crate (incrementally — "Approach C"). It is not yet a data-driven template runtime that diffs without recompiling; sub-second-without-a-rebuild is a bigger future north. For the 90% visual loop, ~2s feels the same.
- The classic native/SSR mode of
fitz devis kill+respawn without browser reload (there's no page to refresh) — the wasm-client mode above is where the browser loop lives. - Running both bins in one
fitz dev(dual-process orchestration) is a follow-up; today it's two terminals.
That's the frontend dev loop closed: a compiled language whose components you edit and see in the browser in a couple of seconds, without losing your state, without an npm ecosystem underneath. Next up: how the same .fitzv renders on the server for first paint, and the WASM runtime then adopts that DOM instead of re-creating it.
Fitz is a compiled language with gradual typing and HTTP/async/DB as first-class citizens, compiling to native binary via Rust. Its frontend is .fitzv single-file components compiled to WebAssembly. Open source.
Top comments (0)