Skip to Content
PlatformsWASM showcase

WASM showcase build

ftui-showcase-wasm wraps the same demo model the native ftui-demo-showcase binary uses, exports a ShowcaseRunner type through wasm-bindgen, and ships as a single .wasm artifact that a browser can load and drive. build-wasm.sh at the repository root is the one-shot build pipeline that produces it.

The runner core (runner_core.rs) is shared between WASM and the native integration tests. If a screen looks correct natively but broken in the browser, the bug is almost always in the backend (see ftui-web), not the screen.

What ships in the bundle

    • lib.rs
    • runner_core.rs
    • wasm.rs
    • ftui_showcase_wasm_bg.wasm
    • ftui_showcase_wasm.js
    • FrankenTerm_bg.wasm (if adjacent crate present)
  • build-wasm.sh
  • frankentui_showcase_demo.html
  • runner_core.rs — platform-neutral runner. Drives the demo screens on top of an ftui-web backend. Also linked into native tests.
  • wasm.rs#[wasm_bindgen] surface. Exports ShowcaseRunner with methods for pushing input JSON, advancing time, ticking, and reading back patches. The full JS contract lives in docs/spec/wasm-showcase-runner-contract.md.

The optimization pipeline

build-wasm.sh is a three-stage size-first pipeline. Size matters: every byte of .wasm is a byte the browser downloads, parses, and compiles before the demo is interactive.

Stage 1 — Rust compiler settings

The release profile is tuned for size, not speed:

[profile.release] opt-level = "z" lto = true codegen-units = 1 panic = "abort" strip = "symbols"

The combination cuts binary size roughly in half versus defaults. The trade-off is longer compile time — the demo is not in any hot build loop.

Stage 2 — WASM-targeted RUSTFLAGS

The script exports CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS, not plain RUSTFLAGS. This matters: RUSTFLAGS also applies to build-script and proc-macro compilations that run on the host toolchain, which then fails to recognize WASM-only target features. The target-scoped variant only affects the wasm32 target.

-C target-feature=+bulk-memory,+mutable-globals,+nontrapping-fptoint,\ +sign-ext,+reference-types,+multivalue
  • bulk-memorymemcpy / memset as single WASM instructions
  • mutable-globals — cheaper thread-local-like patterns
  • nontrapping-fptoint — faster float-to-int conversions without trap checks
  • sign-exti32.extend8_s / extend16_s instead of shift pairs
  • reference-types — required by some wasm-bindgen paths
  • multivalue — functions can return multiple values without spilling

All of these are supported in Chrome 91+, Firefox 89+, and Safari 15+, which matches the showcase’s stated browser baseline.

Stage 3 — wasm-opt with --converge

wasm-pack automatically invokes wasm-opt (from binaryen ) with the flags from each crate’s [package.metadata.wasm-pack.profile.release]:

wasm-opt = ["-Oz", "--all-features", "--converge"]

--converge reruns the entire pass pipeline until one full pass produces no further improvement. It is slow, but for a demo artifact that ships once per release it is free money in download size.

The Cargo.toml patch trick

The workspace has:

# VFX-heavy crate: prefer speed over binary size [profile.release.package.ftui-extras] opt-level = 3

That is the right call for the native release build — the VFX kernels in ftui-extras are real hot loops. But it is catastrophic for WASM size: -O3 aggressively inlines, unrolls, and duplicates paths, adding megabytes. The build script therefore:

  1. Copies Cargo.toml to Cargo.toml.bak.
  2. Uses sed to remove the [profile.release.package.ftui-extras] section and its comment.
  3. Runs the WASM build under the patched manifest.
  4. Restores the original on exit (via a trap).

You never see the patched manifest in git — if the script aborts mid-run, the trap still restores.

Running the build

# one-time setup cargo install wasm-pack # build ./build-wasm.sh # serve python3 -m http.server 8080 # then open http://localhost:8080/frankentui_showcase_demo.html

The script reports each artifact’s size at the end:

── WASM binary sizes ── pkg/ftui_showcase_wasm_bg.wasm: 3.14 MB (3292284 bytes)

Running the bundle

frankentui_showcase_demo.html in the repo root is the canonical host page. It:

  1. Imports the wasm-bindgen-generated JS shim from pkg/.
  2. Instantiates a ShowcaseRunner against a canvas or DOM grid.
  3. Pumps input events from the DOM into runner.push_input(json).
  4. Each animation frame, calls runner.advance_time(dt) then runner.tick(), then reads back patches and repaints.

Because the clock is deterministic and the event queue is explicit, a test harness can replay recorded input JSON and compare patch hashes byte-for-byte against a golden run. This is how web-side regressions get caught.

The adjacent build

The script also builds crates/frankenterm-web if it is present:

wasm-pack build crates/frankenterm-web --target web --release

That crate is not in this workspace (see integration note); the step is a no-op in a stock FrankenTUI checkout and active in the FrankenTerm monorepo that vendors both.

Pitfalls

  • Do not enable opt-level = 3 for the whole release profile. It may feel tempting “for the demo”, but it makes the bundle 2–3× larger with no meaningful runtime win — the render pipeline is already tuned for cache locality, not raw ILP.
  • Do not drop --converge. A single -Oz pass leaves substantial dead code behind after LTO; converge chases it.
  • Do not move WASM flags into plain RUSTFLAGS. Host-compiled proc macros will choke on +bulk-memory.
  • Do not check in Cargo.toml.bak. It should never appear in a clean tree; if it does, the last build aborted without running its trap.

See also