Elm → WebAssembly with elmc
elmc lowers the same Elm sources this site uses for elm-pages into a WASM module and a thin JS host. Browser programs boot, update, and render in the host — without shipping Pebble draw/cmd/sub specials into the web graph.
At a glance
- CompilesIR → plan → WASM + minified host
- RunsBrowser.* apps, Html/Svg, Http, Time, Task, ports
- This sitenpm run serve:wasm boots the SPA shell
- Demo belowelm-3d-scene / WebGL on the WASM path
Live demo
A Pebble-like watch with the Elm tangram (square) on the dial and orbiting logo-colored spheres — rendered with ianmackenzie/elm-3d-scene over elm-explorations/webgl.
No physics solver: procedural orbits on Time.every 64 (~16 Hz) so the rest of the page stays responsive. The same scene is gated on the WASM host with npm run verify:wasm:hero.
Drag to orbit. Watch face is the Elm tangram (square) — ianmackenzie/elm-3d-scene over elm-explorations/webgl.
What the WASM build is
Reachable package code is compiled into the module; unused code can be stripped. There is no separate “port package X to WASM” step — only what the app graph needs. BackendTask route data is evaluated at compile time in Elixir; the browser loads the WASM client plus the thin host.
Official site build
npm run build → elm-pages + official Elm → JS in dist/.
WASM parity build
npm run build:wasm → elmc --target wasm --web → dist/wasm-web/ (app.wasm, host/boot.js, manifest).
Strict web apps
Browser.* / elm/*-only apps keep wasm_strict: true so Pebble specials never enter the web dispatcher.
This elm-pages site
Builds with wasm_strict: false because the combined graph still carries some Pebble-only plan ops; browser-relevant stubs stay gated empty.
What already lowers
The WASM web surface boots real Browser programs end-to-end: init/update/view, subscriptions, cmds, and page data for this elm-pages site. Pebble UI, Cmd, and Events stay out of the web graph.
Browser programs
sandbox, element, document, application, and Platform.worker.
Virtual DOM
Html, Svg, Keyed, Lazy, Html.map, and event handlers.
Browser APIs
Events, Navigation, Dom.focus / viewport / setTitle.
Effects
Task, Process.sleep, Time.now / every / here, Random, Regex.
Http & files
Http expect/bytes/timeout errors; File.Download / Select; BackendTask.Http via fetch.
Data & decode
Bytes, Json.Decode/Encode (structured errors), Parser, Dict/Set equality.
elm-pages
Multi-route bytes on boot and navigation; incoming/outgoing ports with Sub.map / Cmd.map.
WebGL / Scene3d
Host webgl_* bridges; this page’s HeroScene draws on the WASM path.
Not in web
Pebble UI, Cmd, and Events — filtered from the web dispatcher.
How to build it
From elm_pebble_dev/ or the repo root:
npm run build:wasm npm run verify:wasm npm run verify:wasm:hero npm run serve:wasm # equivalent ./scripts/build-elm-pebble-dev-wasm.sh elmc compile elm_pebble_dev \ --out-dir dist/wasm-web \ --target wasm --web
Artifacts
Under dist/wasm-web/:
wasm/app.wasm(+ optional .br)host/boot.js(bundled host)host/browser.htmlelmc_wasm.manifest.json
Full matrix and boot notes: elmc/docs/WASM_WEB_BUILD.md.
WASM parity
This page’s elm-pages shell boots under elmc WASM (npm run serve:wasm). Index and multi-route page data, ports, and the HeroScene WebGL path are covered by verify:wasm / verify:wasm:hero (and optional Playwright verify:wasm:browser).
Still in motion
RC ownership and Scene3d/WebGL coverage keep widening beyond this demo’s Lambertian meshes. The demo itself stays light on purpose — procedural orbits instead of elm-physics — so docs stay snappy while the toolchain catches up.