Can JIT compilation targeting WebAssembly outperform native code? That’s the question WATaBoy explores, an experimental Game Boy emulator built as a final-year undergraduate project. The answer, backed by benchmarks, is yes.
JIT WebAssembly and Game Boy Emulation: The Context
It all starts with a well-known constraint for iOS developers: Apple prohibits JIT compilation in third-party apps. Dolphin, the GameCube emulator, can’t run on iPhone as a result. The only exception applies to web browsers, which use JavaScriptCore to compile JavaScript and WebAssembly into native machine code.
The author of WATaBoy had the idea to exploit that exception. Instead of generating machine code directly, why not generate WASM bytecode on the fly and let the browser engine compile it to native? That’s already what projects like the Jiterpreter and v86 do, but nobody had applied the technique to console emulation or compared its performance against a natively running interpreter.
WATaBoy’s Technical Architecture
Generating WASM Bytecode from Rust
WATaBoy is written in Rust and compiles to wasm32-unknown-unknown. For runtime bytecode generation, the project uses the wasm-encoder crate, which provides a builder pattern for emitting WASM instructions without manually manipulating raw byte arrays.
The workflow goes like this:
- The JIT recompiles each non-branching Game Boy instruction to form a basic block
- That block becomes a WASM module with an
execute_blockfunction - The module is passed to JavaScript via the C ABI (buffer pointers and lengths)
- JavaScript compiles, instantiates, and links the module into the main module’s indirect function table
- Rust then calls the function via the
call_indirectinstruction
Two LLD flags are essential: --export-table to expose the indirect function table, and --growable-table to allow it to grow at runtime.
The WASM Harvard Architecture Problem
WASM is a Harvard architecture, not von Neumann. You can’t directly execute bytecode generated at runtime from within the runtime itself. You have to go through the host environment (JavaScript) to compile and instantiate the new module, then link it dynamically. That’s the cost of the indirection, and the project shows that this cost is offset by the JIT compilation gains.
To call call_indirect from Rust, WATaBoy uses inline WebAssembly via the unstable asm_experimental_arch feature, since no standard intrinsic covers that instruction.
Cycle-Accurate Emulation Despite the JIT
The Game Boy is a special case: its CPU (the SM83) is far from the main bottleneck in a modern emulator. Cycle-accurate precision makes things even more complex. WATaBoy draws on techniques described by the GameRoy project:
- Interrupt prediction to avoid unnecessary JIT block exits
- Lazy fallback to the interpreter
- Deferred evaluation of non-CPU components accessed via MMIO
Results: The WASM JIT Wins
The benchmark compares three configurations on a MacBook Air M2 running macOS 26.5: JIT-to-Wasm in the browser, interpreter in the browser, and interpreter compiled natively. Three ROMs were used as test cases: Pokémon Blue, Zelda Link’s Awakening, and Tobu Tobu Girl.
On Pokémon Blue, JIT-to-Wasm reaches roughly 1.2x the speed of the native interpreter and 1.5x that of the interpreter running inside WASM. That’s notable: you’re adding a layer of indirection (browser plus WASM runtime) and still coming out faster than the native binary.
On the JS engine side, Safari comes out fastest on this benchmark, which is good news for the iOS angle since WebKit is mandatory there.
Limitations and What’s Next
The author is upfront about the current friction points:
- The PPU (graphics rendering) still accounts for the bulk of execution time, as several PPU interrupts aren’t yet subject to prediction
- Branching instructions aren’t recompiled yet, forcing frequent returns to the interpreter between blocks
- WASM bytecode generation tooling remains hand-rolled: every project writes its own tools, with no equivalent of DynASM or Cranelift for WASM
- Some low-level optimizations like Dolphin’s fastmem are impossible inside the WASM sandbox
Audio and Game Boy Color support are also missing. The project is open source on GitHub and the author plans to keep pushing both approaches to their limits.
Key Takeaways
- A JIT targeting WASM can outperform a native interpreter, even when adding an indirection layer through the browser
- The technique relies on runtime WASM bytecode generation in Rust, dynamic linking via JavaScript, and dispatch through
call_indirect - Safari delivers the best performance on this benchmark, validating the approach for iOS despite Apple’s JIT restriction
- WASM codegen tooling remains the main barrier to broader adoption of this technique in the emulation ecosystem
- WATaBoy is primarily a solid proof of concept, not a consumer-ready emulator, but its results open up real possibilities for cross-platform emulation
If you’re interested in this kind of exploration at the intersection of compilation, runtime security, and execution constraints, there’s more on the blog. Feel free to share your thoughts.

