Skip to content

Rust and WebAssembly

Terminal window
npm install @kadal/deck-wasm
src/main.tsx
registerLazyRenderer("wasm", () => import("@kadal/deck-wasm").then((m) => m.wasmRenderer));

A WebAssembly module can’t reach the DOM, so every WASM UI framework ships a JavaScript shim — and the shim is the only thing a renderer can hold. This renderer drives an agreement rather than a framework:

export interface WasmIslandModule {
default?: () => Promise<unknown>; // the initialiser wasm-bindgen generates
deckMount(host: Element, props: IslandProps): WasmIslandHandle | Promise<WasmIslandHandle>;
}
export interface WasmIslandHandle {
update?(props: IslandProps): void;
destroy(): void;
free?(): void; // wasm-bindgen's, called after destroy when present
}

In Rust it is one attribute:

#[wasm_bindgen(js_name = deckMount)]
pub fn deck_mount(host: HtmlElement, props: JsValue) -> TickerHandle { /* … */ }

Build with wasm-bindgen --target web and load the generated module through wasmBindgenModule:

import { wasmBindgenModule } from "@kadal/deck-wasm";
<Island
framework="wasm"
load={() => import("./ticker/pkg/ticker.js").then(wasmBindgenModule)}
props={{ step }}
/>

Why wasmBindgenModule: a wasm-bindgen module’s default export is its initialiser, not the component. <Island load> unwraps default for every JavaScript framework; here that would call the initialiser and then hand raw WebAssembly a DOM element. The helper says what the module is.

Two things in a Leptos crate driven from outside a router: use ArcRwSignal, not RwSignal (the signals outlive the mount closure), and keep the unmount handle as a Box<dyn Any> (a #[wasm_bindgen] struct can’t carry a generic; dropping the box still runs the real Drop). An optimised Leptos island in the showcase is 107 kB.

Only JSON-shaped values cross into WebAssembly. Unserialisable props are dropped with a warning rather than thrown — one bad prop shouldn’t take the slide down. free() is called after destroy(), because wasm-bindgen’s structs are pointers into linear memory the JavaScript collector can’t see.

This is the only renderer that can honestly implement owns(): a module exporting deckMount said so on purpose. The showcase also carries thirty lines of hand-written WebAssembly text, assembled to 195 bytes, using the same agreement.