DEV Community

Martin Palopoli
Martin Palopoli

Posted on

A Vite-class dev loop for a compiled language: hot reload + state preservation + a live manifest

Part 13 of the Fitz series. The frontend chapter opens: Fitz compiles .fitzv components 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}
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

What it does:

  • Incremental rebuild with wasm-pack --dev (no wasm-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: your index.html, your CSS, the bundle in target/wasm/<bin>/. No index.html? It generates a minimal one at the mount point.
  • Browser auto-refresh over a WebSocket: save a .fitzv/.fitz/fitz.toml and 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> = []
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

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].main to 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

--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 watch rebuilds 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 dev ships 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 dev is 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)