How Safari Runs Wasm

This page answers one question: how does JavaScriptCore, the engine in Safari and every iOS browser, execute WebAssembly — and what does that mean for modules that need to start fast and run well on iPhones and iPads?

Prerequisites

  • [ ] Safari on macOS, and ideally an iPhone or iPad for testing.
  • [ ] Safari’s Web Inspector (enable Show features for web developers in Safari’s Advanced settings).
  • [ ] A module with measurable compile time — a few hundred kilobytes or more.

Three tiers instead of two

JavaScriptCore runs WebAssembly through three tiers. The in-place interpreter (IPInt) executes WebAssembly bytecode almost directly, with very little preparation, so a module can start running before any machine code exists. The BBQ tier (“build bytecode quickly”) is a fast baseline compiler producing simple machine code. The OMG tier (“optimized machine-code generator”) is the optimizing compiler, built on JavaScriptCore’s B3 backend, producing code comparable to other engines’ top tiers.

Functions start in the interpreter and move up as they get hot: frequently called functions are compiled with BBQ, and those that stay hot are compiled with OMG, with on-stack replacement letting long-running loops switch tiers mid-execution. The extra tier at the bottom is the main structural difference from V8 and SpiderMonkey: it lowers startup cost further, which suits mobile devices where compile time is expensive, at the price of slower execution for code that has not yet tiered up.

JavaScriptCore's WebAssembly tiers Functions begin in the in-place interpreter, which needs almost no preparation. Warm functions are compiled by BBQ into fast-to-produce machine code, and hot functions by OMG into optimized code. Each step trades more compile time for faster execution. OMG (optimizing) B3 backend; slowest to compile, fastest code; hot functions BBQ (baseline JIT) quick machine code for warm functions IPInt (in-place interpreter) runs bytecode immediately; slowest execution

Step 1 — know the startup profile this creates

Because functions can run in the interpreter immediately, Safari can make a module usable very quickly after download, with compilation work spread over the first moments of execution rather than concentrated before it. For short tasks — a function called once at startup — the interpreter may be all that ever runs it. For sustained work, code moves to BBQ and then OMG over the first fractions of a second.

In practice that means two things for authors. A module that does a large burst of work immediately after instantiation spends the beginning of that work in slow tiers; a short warm-up or a smaller first task lets it reach optimized code before the heavy lifting. And benchmarks that measure only the first iterations in Safari measure the interpreter and BBQ, not the engine’s best code — the same warm-up caution that applies everywhere, more pronounced here.

Step 2 — profile in Web Inspector

Open Web Inspector’s Timelines tab, enable the JavaScript & Events timeline, and record the interaction. WebAssembly functions appear in call trees by name if the module kept its name section. Safari’s tooling exposes less about compiler tiers than Chrome’s or Firefox’s, so the practical signal is per-call timing over time: record a few seconds, and watch the time per call of the hot function fall as it moves up the tiers.

For device performance — what users on iPhones experience — profile on a device connected to a Mac. Laptop numbers say little about mobile CPUs, memory bandwidth or thermal limits; the device setup is covered in testing a Wasm app on a phone during development.

Per-call time of a hot function as it moves through JavaScriptCore's tiers A compute-heavy function called repeatedly after load on an iPhone. It starts in the interpreter, speeds up after BBQ compilation, and reaches its fastest time once OMG code is in place. 1 calls IPInt: 21 ms/call 12 calls BBQ code: 6.1 ms/call 140 calls OMG code: 2.2 ms/call 300 calls steady: 2.2 ms/call

Step 3 — mind Safari’s limits and policies

Some constraints apply specifically on Apple platforms. On iOS, every browser uses JavaScriptCore, so Chrome and Firefox on iPhone behave like Safari for WebAssembly. Memory available to a tab is more limited on mobile devices, and iOS may terminate a tab that grows too large without the warnings a desktop would give; size modules’ memory conservatively, as in sizing initial and maximum memory. Lockdown Mode disables JIT compilation and WebAssembly entirely, which affects a small but significant group of users. And newer proposals may arrive in Safari at different times than in other browsers, so detect features rather than assuming them.

Step 4 — check feature support for Safari

Before relying on a proposal — SIMD, threads, exception handling, GC, relaxed SIMD — check whether the Safari versions you support implement it, and detect at runtime with validation probes, as shown in detecting proposal support at runtime. Threads additionally need cross-origin isolation, which Safari supports with the same COOP and COEP headers as other browsers. A build matrix of “newest features” and “conservative” variants, chosen at load time, keeps older Safari versions working.

What to check specifically for Safari and iOS Areas where Safari and iOS can differ from Chromium-based browsers for WebAssembly, why each matters, and what to do about it. area why it matters action all iOS browsers use JavaScriptCore test Safari = test iOS memory limits tabs killed earlier smaller maximum memory Lockdown Mode no JIT, no Wasm degraded path proposal support may arrive later detect, ship fallback interpreter tier slow first iterations warm up before heavy work

Step 5 — design for a fast start on mobile

The tiering design rewards modules that let hot code warm up and that keep startup work small. Split startup so the first visible work uses a small amount of code, and defer heavy computation until after the first interaction or until the page is idle. Keep modules small so download and any up-front work stay short. Use streaming compilation and stable, cacheable URLs so returning users benefit from cached compiled code. None of this is Safari-specific, but Safari on older iPhones is often the environment where these choices make the largest difference to what users feel.

How the three engines compare in practice

With three major engines using different tier structures — V8’s two compilers with on-demand optimization, SpiderMonkey’s two compilers with eager background optimization, JavaScriptCore’s interpreter plus two compilers — it is tempting to look for one that is “fastest”. In steady state, all three produce well-optimized code and the differences for typical kernels are modest, often within 10–20%. The differences that users notice are in the first second: how quickly a module becomes usable, and how quickly hot code reaches full speed. Those depend on the engine’s strategy, the device and the module’s size and structure. The only reliable answer for a given module is to measure cold start and warm performance in each engine, on the devices your users actually have.

Finally, include at least one older iPhone in the devices you test on. The tiering design narrows the gap between old and new hardware for startup, but steady-state performance on a four-year-old device can still be several times slower, and that device is the one your most constrained users hold.

Expected output

A Web Inspector recording of the workload on an iPhone shows per-call times for the hot function dropping in steps over the first second, ending at a steady time comparable to Chrome on Android hardware of a similar class.

Gotchas

  • Testing iOS performance in desktop Safari. The engine is the same; the hardware is not. Test on devices.
  • Assuming Chrome on iOS uses V8. It does not. All iOS browsers use JavaScriptCore, so results from Chrome on Android do not carry over.
  • Heavy work immediately after instantiation. It starts in the interpreter. Warm up first or defer it.
  • Feature detection by user agent. Safari versions and their Wasm features do not map cleanly to UA strings. Validate probes instead.
  • Large maximum memory on iOS. Tabs may be terminated. Keep memory use modest and handle allocation failure.

Performance note

On an iPhone, a 1.8 MB image module was usable 90 ms after download — the interpreter let it start before compilation finished. Its main filter ran 9.5× slower in the interpreter than after reaching OMG code, which it did after about 140 calls. Splitting the first operation into a small preview pass followed by the full-resolution pass let the expensive work run almost entirely on optimized code.

Filter time per call by tier on an iPhone Per-call time of an image filter in each JavaScriptCore tier on a recent iPhone, measured by forcing early calls and then waiting for tier-up. ms per call (lower is better) in-place interpreter 21 ms BBQ 6.1 ms OMG 2.2 ms

Frequently Asked Questions

Does Safari cache compiled WebAssembly? Caching behaviour has varied across releases; use stable, cacheable URLs and measure returning-visit startup on the versions you support.

Do Safari’s tiers affect SIMD code? SIMD instructions are supported in the compiled tiers; very short runs may execute in the interpreter first. Hot SIMD kernels reach optimized code like any other function.

Is the interpreter a performance problem? Only for code that runs briefly and heavily. For most modules it shortens startup and hot code moves to optimized tiers quickly.

Can I disable tiers in Safari for testing? Not from page settings. JavaScriptCore has environment variables for engine developers; for application testing, warm up and measure steady state.

Why does my module feel slower in Safari only at first? Most likely the interpreter and BBQ tiers handling the first calls. Measure after warm-up; if steady-state speed matches other engines, the issue is startup shape, and deferring heavy work fixes it.

Do WebKit-based browsers on Linux behave the same? They use JavaScriptCore too, so the tier structure is the same, though platform support for some features may differ.

← Back to Engine Tiering & JIT Compilation