Supporting Older Safari Versions with Wasm

This page answers one task: analytics show a meaningful share of users on older Safari — iPhones that no longer receive iOS updates, Macs on older macOS — and your WebAssembly module, built with current toolchain defaults, fails for them with compile errors. You want a strategy that keeps the feature working there without holding back everyone else.

Prerequisites

  • [ ] Analytics showing Safari and iOS version distribution for your users.
  • [ ] Control over the module’s build flags (Rust target features, Emscripten flags, Binaryen features).
  • [ ] Access to an older iOS device or a testing service with older Safari versions.

Why old Safari matters for Wasm

Safari’s version is tied to the operating system: iOS users get Safari updates with iOS updates, and devices that can no longer update iOS stay on the last Safari version their iOS supports. Every browser on iOS uses WebKit, so even Chrome or Firefox on an old iPhone runs the old engine. A share of users therefore remains on Safari versions several years old.

WebAssembly’s core (the MVP) has been supported for many years, but toolchains now enable post-MVP features by default: sign-extension operators, mutable globals, non-trapping float-to-int conversions, bulk memory, multi-value, reference types — and optionally SIMD, threads, exception handling, tail calls and GC. Each arrived in Safari at a different version. A module that uses any feature the engine lacks fails to compile with a CompileError, usually mentioning an unknown opcode or invalid section, and the feature does not run at all.

When Wasm features became available in Safari (approximate) The Wasm MVP shipped in Safari 11. Features such as sign extension, bulk memory, non-trapping conversions and reference types arrived over the following versions, SIMD around Safari 16.4, and exception handling, tail calls and GC more recently. Older devices stuck on earlier iOS versions lack the later features. 11 Safari version MVP 14 Safari version sign-ext, bulk memory, etc. 15 Safari version reference types, multi-value 16.4 Safari version SIMD 18 Safari version exceptions, tail calls, GC (recent)

Version numbers in this timeline are approximate; check a current compatibility table (such as the WebAssembly feature status page) for exact versions before setting your baseline.

Step 1 — decide the oldest version you support

Look at your analytics: the share of sessions on each Safari major version, ideally weighted by how much those users use the Wasm feature. Pick a cut-off where the remaining old-version share is small enough to accept a fallback (or no feature) for them. Write the decision down with the date and numbers; it will need revisiting as old devices disappear.

Step 2 — build a baseline module for that version

Build a module that uses only features available in your oldest supported Safari. In Rust, disable target features enabled by default in recent compilers:

RUSTFLAGS="-C target-cpu=mvp -C target-feature=+mutable-globals,+sign-ext" \
  cargo build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abort   # nightly: rebuild std without newer features

Rebuilding the standard library is needed because the precompiled std may use newer features; check current Rust documentation, as defaults and the mechanism change between versions. In Emscripten, set -sMIN_SAFARI_VERSION= to the oldest version you support; Emscripten then avoids features that version lacks and lowers others. Run wasm-opt with matching feature flags so it does not introduce newer instructions.

Step 3 — verify the module’s features

Check what the built module actually uses:

wasm-tools validate --features mvp,mutable-global,sign-extension baseline.wasm   # fails if anything newer slipped in
wasm-tools print baseline.wasm | grep -E "memory.copy|memory.fill|v128|try|return_call" | head

Validating with a restricted feature set is the reliable check; grepping for instructions is a quick sanity test.

One modern build versus modern plus baseline builds A single module built with current defaults is smallest to maintain but fails to compile on older Safari. Shipping a modern build plus a conservative baseline build chosen by feature detection keeps the feature working on older devices at the cost of building and testing two artefacts. one modern build current toolchain defaults CompileError on old Safari simplest pipeline drops old devices modern + baseline builds feature-detect at load old Safari gets baseline two builds to test broad support

Step 4 — feature-detect and load the right build

At load time, detect the features the modern build needs (with small validation probes, for example from the wasm-feature-detect package) and load the modern build if all are present, otherwise the baseline. Do not sniff user agents — feature detection is exact and keeps working as devices update. See serving different Wasm builds per browser.

Step 5 — respect older devices’ limits

Older iPhones have less memory, and Safari’s limits on Wasm memory are tighter there. A module that reserves a large initial memory or grows to hundreds of megabytes may be killed. In the baseline build, use smaller initial memory, process data in chunks, and avoid threads (older Safari versions lacked SharedArrayBuffer support or cross-origin isolation features). Performance is also lower: SIMD-less builds run vectorised code paths several times slower, so consider reducing work (lower resolution, smaller batches) when the baseline build is in use.

Testing on real old versions

Simulators and current devices do not reproduce old WebKit. Keep one or two old devices on their final iOS versions, or use a device cloud that offers them, and run the feature there before releases. Automated WebKit tests in CI use current WebKit and will not catch old-version gaps.

JavaScript glue matters too

The module is only half of what loads. Toolchain glue — wasm-bindgen’s output, Emscripten’s runtime — is JavaScript, and modern toolchains emit modern syntax and APIs: optional chaining, BigInt usage for 64-bit integers, FinalizationRegistry, WeakRef, top-level await in ES modules. Older Safari versions lack some of these, so a baseline Wasm build can still fail on a SyntaxError in its glue. Emscripten’s MIN_SAFARI_VERSION setting also controls the JavaScript it emits, and can transpile with Babel where needed; for wasm-bindgen output, run the glue through your bundler’s transpilation targeting the same browsers. Test that the glue parses on the oldest version — a syntax error prevents the whole script from running, which often looks like the feature silently missing rather than an error you will notice. Avoid relying on BigInt across the boundary in the baseline build if very old versions matter.

Communicating with users on unsupported versions

Below your support cut-off, decide what users see. A clear message (“This feature needs a newer version of iOS”) with a server-side alternative, if you have one, is better than a broken button. Feature detection makes this straightforward: if neither the modern nor the baseline build can run, show the alternative instead of attempting to load anything. Track how many users see the message; when the number becomes negligible, raising the baseline is an easy decision, and when it is significant, it justifies continuing to maintain an older build.

Raising the baseline over time

Review the baseline every few months. As old devices disappear from analytics, raise the minimum version, enable more features in the baseline build, and eventually retire the second build entirely, which removes a whole branch of testing.

Tracking which build users get

Report which build each session loaded alongside performance numbers, so you can see how many users run the baseline and how their experience compares.

Expected output

Analytics show 4% of iOS sessions on Safari versions without SIMD; the app ships a modern build (SIMD, bulk memory, reference types) and a baseline build validated against the oldest supported feature set; feature detection selects the baseline on old devices, which run the feature about 2.5× slower with reduced batch sizes; and an iPhone kept on an old iOS version runs the release checklist.

Gotchas

  • Assuming toolchain defaults are conservative. They enable newer features. Set targets explicitly.
  • Forgetting the standard library. Precompiled std may use new features. Rebuild or check.
  • wasm-opt adding newer instructions. Pass the same feature flags.
  • User-agent sniffing. Inexact and brittle. Feature-detect.
  • Testing only on current devices. Old WebKit differs. Keep an old device.
  • Modern syntax in the glue. A SyntaxError stops everything. Transpile glue for old targets.

Performance note

On an older iPhone, the baseline (non-SIMD) build of an image filter took 2.6× as long as the SIMD build on a current iPhone of the same class, so the app halves preview resolution when the baseline build is loaded.

Filter time by build on iPhones Milliseconds per filter pass for the SIMD build on a recent iPhone, and the baseline build without SIMD on an older iPhone running an older Safari, for the same image size. ms per filter pass SIMD build, recent iPhone 24 ms baseline build, older iPhone 62 ms

Frequently Asked Questions

Can I polyfill missing Wasm features? Not at runtime; you lower them at build time (Binaryen can lower some) or avoid them.

Is wasm2js still useful? For engines without Wasm at all, rarely needed now; for feature gaps, a baseline Wasm build is better.

How long should I support old versions? Until their share of feature usage falls below a threshold you set; review periodically.

Do other iOS browsers help? No — they all use WebKit on iOS.

Why does the baseline build still fail on old Safari? Often the JavaScript glue uses syntax or APIs the old version lacks; transpile the glue for the same browser targets.

What should users below the cut-off see? A clear message and, if available, a server-side alternative instead of a broken feature.

When can the baseline build be retired? When analytics show old versions’ share of feature usage below your threshold; review every few months.

Does BigInt matter for old Safari? Yes — very old versions lack it, so avoid 64-bit integers at the boundary in builds for them.

Should analytics record the loaded build? Yes — it shows how many users run the baseline and how their performance compares.

← Back to Polyfill Alternatives & Fallbacks