moxzi
Docs / Browser / Motoko in a browser tab

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.

PieceWhat it isWho owns it
The canisterYour .wasm from moxzi build / moc — memory64 + enhanced orthogonal persistenceJavaScript: WebAssembly.Instance, WebAssembly.Memory, the table
The hostmoxzi_web_bg.wasm, wasm32, built from moxzi/web/src/lib.rsRust: HostState, the scheduler, and an independent implementation of the management-canister interface (see provenance)
The seamAn import object whose ic0.* entries call into the host modulemoxzi/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#

CapabilityVerified by
Install, call, query, upgrade actorsscripts/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/recurringTimerscripts/web_timer_gate.sh
Whole-runtime persistence to IndexedDB, spawned actors includedscripts/web_durable_gate.sh
Real HTTPS outcalls through http_request, transform includedscripts/web_outcall_gate.sh
Killing a runaway message and rebuilding the runtimescripts/web_lib_gate.sh
The Motoko compiler itself, in-tab, output byte-identical to nativescripts/web_page_compile_gate.sh
mo: package imports compiled in-tab, byte-identical to moxzi buildscripts/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#

DifferenceWhy
No certificatesic0.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-inV8 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 coarseA 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 farA 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#

On this pageWhat is actually in the pageWhat it can doHow close to nativeWhat is deliberately not the sameWhere the compiler runs, and where the actors runNext