How SpiderMonkey Compiles Wasm
This page answers one question: how does SpiderMonkey, Firefox’s JavaScript and WebAssembly engine, turn a module into machine code — and how does that differ from Chrome’s V8 in ways that show up in your measurements?
Prerequisites
- [ ] Firefox (current release) and the Firefox Profiler (built in, reached from DevTools’ Performance panel).
- [ ] A module large enough for compile time to be visible — a few hundred kilobytes or more.
- [ ] Optionally, the V8 comparison in how V8 compiles Wasm with Liftoff and TurboFan.
Baseline and Ion
SpiderMonkey also uses two compilers for WebAssembly. The baseline compiler is a single-pass compiler designed for speed: it translates each function’s bytecode directly into machine code, keeping a simple model of where values live and emitting straightforward instructions. It is fast enough that a large module compiles in tens of milliseconds, so execution can begin quickly.
Ion is SpiderMonkey’s optimizing compiler, shared with its JavaScript JIT. For WebAssembly it builds an intermediate representation, applies optimizations such as global value numbering, loop-invariant code motion and bounds-check elimination, and performs register allocation. Ion code is substantially faster than baseline code for compute-heavy functions, and slower to produce.
The difference from V8 is mostly in when each runs. SpiderMonkey has historically compiled modules with the baseline compiler for immediate use and started compiling the whole module with Ion on background threads straight away, replacing baseline code as Ion finishes — rather than waiting for functions to become hot. That means more background work up front, and optimized code arriving sooner for every function, hot or not. Engine strategies evolve between releases, so measure on the versions you care about rather than relying on a fixed description.
Step 1 — see streaming compilation in Firefox
Firefox compiles while downloading when you use instantiateStreaming or compileStreaming, just as Chrome does: the baseline compiler works on
functions as their bytes arrive, so the module is ready soon after the last byte. In the Firefox Profiler, record a page load and look in the
markers for WebAssembly compilation tasks on helper threads — they overlap the network request for the module when streaming is working.
If the server sends the module with the wrong MIME type, streaming fails and glue code falls back to ArrayBuffer compilation, and the overlap
disappears. The fix is a server header, covered in
serving Wasm with the correct MIME type locally.
Step 2 — profile and find the tier transitions
Record a profile of the workload in the Firefox Profiler. In the call tree, WebAssembly functions appear by name if the module has a name
section. Compilation work appears as markers and as samples on helper threads. A useful pattern is to record the first few seconds after load and
look at how per-call time changes: if a hot function’s samples drop sharply at some point, that is optimized code replacing baseline code.
WebAssembly compilation markers (helper threads)
Wasm baseline compile 0 – 46 ms
Wasm Ion compile (tier-2) 46 – 690 ms (background)
Main thread
blur_row samples/call high until ~700 ms, then ~3× lower
Step 3 — compare a benchmark across engines
Because the engines tier differently, a short benchmark can tell opposite stories. A test that runs for 100 ms after load may see Firefox on optimized code sooner, because Ion compiled everything in the background, while Chrome is still on Liftoff for functions that have not crossed their tier-up threshold. A test that runs for several seconds after warm-up usually sees both engines on optimized code, with differences that reflect code quality rather than timing. Report which phase a benchmark measures — cold, warming, or steady — so cross-engine comparisons are meaningful, as discussed in avoiding JIT warm-up errors in Wasm benchmarks.
Step 4 — understand caching in Firefox
Firefox can cache compiled WebAssembly code for modules loaded through streaming APIs from cacheable URLs, so a returning visitor’s module is ready
without recompiling. The cache entry is tied to the response in the HTTP cache, which makes stable, immutable, content-hashed URLs important — the
same advice as for Chrome, covered in
versioning Wasm files with content hashes.
Responses with no-store, or served under a different URL on each deploy, defeat it.
Step 5 — test Firefox-specific behaviour
Most WebAssembly behaviour is specified exactly, so engine differences are about speed and limits rather than results. The differences worth testing in Firefox are: compile time and memory use for very large modules, where the background Ion compilation of the whole module uses more CPU and memory early on; support for newer proposals, which may land in Firefox at a different time than in Chrome; and engine limits such as maximum memory and stack depth. Run the application’s smoke tests in Firefox as part of CI, as in testing fallback paths in CI.
Why the engines made different choices
Neither strategy is simply better. Optimizing eagerly in the background gives every function good code soon and makes performance more predictable early in a page’s life, which suits modules where many functions matter — games, emulators, large applications. It costs background CPU and memory even for code that never becomes hot, which matters on low-end devices and for very large modules. Optimizing on demand spends optimization effort only where it pays and keeps startup lighter, at the cost of a warm-up period where hot code runs slower. Engines adjust these trade-offs over time, and both teams have changed their heuristics across releases. For module authors the practical lesson is the same either way: ship small modules, keep hot code concentrated, use streaming and caching, and measure in every engine you support.
When a performance report arrives from Firefox users specifically, start by checking whether it concerns the first second or steady state. The first points at compilation strategy and module size; the second at the code itself, which should then be visible in a profile from any engine.
Expected output
A Firefox Profiler recording of the workload shows baseline compilation finishing within a few tens of milliseconds of the download, Ion compilation continuing on helper threads, and the hot function’s per-call samples dropping once Ion code is in place.
Gotchas
- Comparing a 100 ms benchmark across engines. It measures tiering strategy, not code quality. Run long enough to reach steady state, or report cold numbers explicitly.
- High CPU after load in Firefox. Background Ion compilation of a large module. Expected; it ends once the module is compiled.
- Missing function names in the Profiler. The
namesection was stripped. Profile a build that keeps it. - Treating profiler markers as exact. Marker timing includes scheduling on helper threads. Use them to see order and overlap, not precise costs.
- Assuming identical heuristics across versions. Tiering strategies change. Re-measure after browser upgrades that matter to you.
Performance note
For the same 3 MB module on the same laptop, baseline compilation took 46 ms in Firefox and 41 ms in Chrome; steady-state speed of the hottest kernel differed by less than 10% between the engines once both were optimized. The largest difference was in the first second: Firefox ran the kernel on optimized code sooner, Chrome finished background work earlier and used less CPU during it.
Frequently Asked Questions
Does Firefox have a WebAssembly interpreter? Not for normal execution; the baseline compiler fills that role. Interpreters appear in other engines and in debugging configurations.
Can I disable a tier in Firefox?
about:config exposes preferences such as javascript.options.wasm_baselinejit and javascript.options.wasm_optimizingjit. Use them only for
experiments; see pinning a compiler tier for benchmarks.
Does background Ion compilation compete with my workers? It uses helper threads, which share the CPU with your workers. On machines with few cores, a large module’s background optimization can briefly slow worker-heavy startup; deferring worker-intensive work by a moment avoids the overlap.
Is Cranelift used in Firefox? Cranelift was explored as a WebAssembly backend in Firefox and is now primarily used in wasmtime; Firefox’s optimizing tier is Ion.
Will my module use more memory in Firefox? Briefly, during background optimization of a large module. Once compilation finishes, memory use settles; measure peak and steady usage separately.
Does Firefox optimize Wasm differently from JavaScript? It shares Ion but has a separate frontend for WebAssembly, and Wasm code never deoptimizes because its types are fixed.
Related
- Debugging Wasm in Firefox DevTools — the Profiler and debugger in detail.
- How Safari runs Wasm — the third major engine.
- Why the first call into Wasm is slow — the cold-call costs across engines.
- Measuring Wasm compile time in DevTools — measuring the baseline phase.
← Back to Engine Tiering & JIT Compilation