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.

Two engines, two tiering strategies V8 compiles everything with its baseline tier and optimizes functions only after they become hot. SpiderMonkey compiles with its baseline tier and, in its typical configuration, starts optimizing the whole module in the background immediately, so optimized code arrives sooner for all functions at the cost of more background work. V8 (Chrome, Node) Liftoff for every function TurboFan only for hot functions optimized code after warm-up less background work tier up on demand SpiderMonkey (Firefox) baseline for fast start Ion for the module in the background optimized code soon for all functions more background CPU early on tier up eagerly

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.

When optimized code arrives in each engine For the same 3 MB module on the same laptop, Firefox runs baseline code first and has Ion code for the whole module after about 700 ms of background compilation; Chrome runs Liftoff code and optimizes each hot function after it has been called enough times. 50 ms both: baseline code ready 700 ms Firefox: Ion done for module 900 ms Chrome: hottest functions optimized 1,800 ms Chrome: remaining hot code optimized

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 name section 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.

Baseline compile and steady-state kernel time in two engines Baseline compile time for a 3 MB module and steady-state run time of its hottest kernel in Firefox and Chrome on the same laptop. milliseconds Firefox baseline compile 46 ms Chrome Liftoff compile 41 ms Firefox kernel, steady 12.4 ms Chrome kernel, steady 11.8 ms

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.

← Back to Engine Tiering & JIT Compilation