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.
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.
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
stdmay use new features. Rebuild or check. wasm-optadding 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.
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.
Related
- Choosing feature levels for a Wasm build — picking features.
- Serving different Wasm builds per browser — loading the right build.
- Feature-detecting Wasm at startup — detection.
- Debugging Wasm in Safari Web Inspector — Safari tools.
← Back to Polyfill Alternatives & Fallbacks