Motoko in a browser tabalpha
What actually runs in the page, and how close is it to native?
moxzi-web is the Motoko actor runtime compiled to wasm32 and driven from JavaScript, so a canister module executes in the browser's own WebAssembly engine with the Internet Computer's message semantics around it. The constraint that shapes everything else: the page is not a replica, so anything a replica proves — certificates above all — is absent rather than simulated.
What is actually in the page#
A tab running moxzi-web holds two wasm modules that never touch each other directly.
| Piece | What it is | Who owns it |
|---|---|---|
| The canister | Your .wasm from moxzi build / moc — memory64 + enhanced orthogonal persistence | JavaScript: WebAssembly.Instance, WebAssembly.Memory, the table |
| The host | moxzi_web_bg.wasm, wasm32, built from moxzi/web/src/lib.rs | Rust: HostState, the scheduler, and an independent implementation of the management-canister interface (see provenance) |
| The seam | An import object whose ic0.* entries call into the host module | moxzi/web/lib/moxzi.js builds it per install |
The host module's doc comment states the division plainly: JS supplies the import object, and each of its 48 ic0.* entries lands on the same moxzi_runtime::sys::shim function the native host calls. Nothing about IC semantics is written twice, which is why "the browser behaves like the server" is a structural claim and not a promise to keep two codebases in sync.
The two modules meet through JS because they must: a wasm32 host cannot address a memory64 guest's linear memory, so the host takes a Uint8Array view of the guest's memory instead of a pointer. That view is re-fetched on demand, because memory.grow detaches every existing view and a detached one reports length 0 rather than throwing.
What it can do#
| Capability | Verified by |
|---|---|
| Install, call, query, upgrade actors | scripts/web_lib_gate.sh, scripts/web_actor_gate.sh |
Inter-actor calls, await, re-entrancy (A→B→A) | scripts/web_async_gate.sh, scripts/web_reentrancy_gate.sh |
Actors that spawn actors (management canister: create_canister, install_code) | scripts/web_spawn_gate.sh |
Timers, setTimer/recurringTimer | scripts/web_timer_gate.sh |
| Whole-runtime persistence to IndexedDB, spawned actors included | scripts/web_durable_gate.sh |
Real HTTPS outcalls through http_request, transform included | scripts/web_outcall_gate.sh |
| Killing a runaway message and rebuilding the runtime | scripts/web_lib_gate.sh |
| The Motoko compiler itself, in-tab, output byte-identical to native | scripts/web_page_compile_gate.sh |
mo: package imports compiled in-tab, byte-identical to moxzi build | scripts/web_mops_gate.sh |
How close to native#
Two measurements carry the claim, and both are differential rather than self-reported.
Behaviour. scripts/web_corpus_gate.sh runs moc's corpus programs against the browser runtime in V8 and against the native runtime, and compares replies and prints step by step: 149 programs compared, 0 diverging. The oracle is the native runtime rather than a replica, because differential-corpus.mjs has already established native == replica across the corpus, so browser == replica follows by transitivity. Programs the browser cannot host are named, not counted as agreement — one program (region0-rts-stats) runs on neither host, because it asserts the replica's exact physical stable-memory size at canister_init.
Compilation. scripts/web_page_compile_gate.sh drives headless Chrome, compiles a Motoko program with the compiler canister hosted on the page's main thread, links the result and compares it byte-for-byte with a fully native moxzi build of the same source. The comparison is on bytes because "it produced something" is not a claim worth making.
What is deliberately not the same#
| Difference | Why |
|---|---|
| No certificates | ic0.data_certificate_present answers 0 — the same answer an update gets on the IC. There is no subnet and no key, so a certificate could only be forged, and a forged one is worse than none because code verifies it and believes it. |
| Per-instruction metering is opt-in | V8 has no fuel, so meter: true rewrites the module to count its own instructions and trap when it runs out. Off by default because it costs about a fifth in size and half again in speed. Creation and message fees are charged either way, because programs observe them. |
| Preemption is wall-clock and coarse | A page cannot interrupt a running wasm call. A worker can be terminated, and a terminated worker cannot say where it got to, so the rewind is to the last completed message. |
| Deep expression nesting compiles less far | A guest frame on V8's stack costs far more than one on wasmtime's. See /docs/web/limits/. |
These are listed as decisions, with the measurement behind each, in /docs/web/limits/.
Where the compiler runs, and where the actors run#
The knights demo (moxzi/web/demo/knights/knights.html) is the clearest illustration, because it uses both placements at once and for stated reasons. The forge — the compiler and linker canisters — runs on the page's main thread, and the garden — the world actor and the knights it spawns — runs in a worker, so a runaway brain can be killed and knights can make outcalls.
That split is forced by a measurement, not taste: a tab's main thread gives roughly 11,154 JS-frame-equivalents of stack and a worker only ~5,303, and after codegen's frame-size fix the compiler needs about 8,426. The compiler therefore fits a page and does not fit a worker, while actors fit comfortably in either.
Next#
- /docs/web/install/ — adding the package to an app, and the one bundler rule that silently breaks workers.
- /docs/web/api/ — every call on
Moxzi, and what changes between page and worker. - /docs/web/limits/ — the honest table of what a tab cannot do.