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 anftui-webbackend. Also linked into native tests.wasm.rs—#[wasm_bindgen]surface. ExportsShowcaseRunnerwith methods for pushing input JSON, advancing time, ticking, and reading back patches. The full JS contract lives indocs/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,+multivaluebulk-memory—memcpy/memsetas single WASM instructionsmutable-globals— cheaper thread-local-like patternsnontrapping-fptoint— faster float-to-int conversions without trap checkssign-ext—i32.extend8_s/extend16_sinstead of shift pairsreference-types— required by some wasm-bindgen pathsmultivalue— 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 = 3That 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:
- Copies
Cargo.tomltoCargo.toml.bak. - Uses
sedto remove the[profile.release.package.ftui-extras]section and its comment. - Runs the WASM build under the patched manifest.
- 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
From source
# 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.htmlThe 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:
- Imports the
wasm-bindgen-generated JS shim frompkg/. - Instantiates a
ShowcaseRunneragainst a canvas or DOM grid. - Pumps input events from the DOM into
runner.push_input(json). - Each animation frame, calls
runner.advance_time(dt)thenrunner.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 --releaseThat 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 = 3for 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-Ozpass 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 itstrap.
See also
- Platforms overview — where this fits in the backend model
- Web backend — what ftui-showcase-wasm drives
- FrankenTerm integration — the adjacent browser bundle
- Runtime overview · Demo showcase — screen catalog · Contributing — dev loop