Degrading Gracefully When Wasm Is Disabled
This page answers one task: make sure the small share of users whose browsers have WebAssembly disabled still get a page that works — or at least one that tells them clearly what is unavailable and why.
Prerequisites
- [ ] A page or app with at least one WebAssembly-powered feature.
- [ ] A list of which features depend on Wasm and which do not.
- [ ] Access to at least one environment with Wasm disabled for testing (see step 5).
Where WebAssembly is turned off
Every current browser engine implements WebAssembly, so “unsupported” mostly means “deliberately disabled”. The reasons are security and policy, and they cluster into a few groups worth knowing.
Hardened modes. Apple’s Lockdown Mode, offered to users at high risk of targeted attacks, disables JIT compilation in Safari, and WebAssembly with it. Some security-focused browser configurations and forks do the same. These modes are rare but their users are exactly the people who should not be shown a broken page.
Enterprise policy. Managed browsers in corporate environments can disable WebAssembly or JIT through administrative policy, typically to reduce attack surface on locked-down workstations. Users there cannot change it.
JIT-less engines. Some embedded web views and kiosk configurations run JavaScript without a JIT. WebAssembly may be absent or run in a slow interpreter.
Content Security Policy. A page’s own CSP without 'wasm-unsafe-eval' blocks compilation even though the API exists — a configuration
problem rather than a user choice, covered in
writing a Content Security Policy for Wasm.
Step 1 — detect by trying, not by sniffing
The global WebAssembly may be missing, present but unable to compile, or present and working. A detection that only checks for the
global misses the second case. Compile a tiny valid module and see what happens:
export async function wasmStatus() {
if (typeof WebAssembly !== "object") return "absent";
try {
const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]); // the smallest valid module
await WebAssembly.instantiate(bytes);
return "ok";
} catch (err) {
return err instanceof WebAssembly.CompileError || /Content Security Policy|unsafe-eval/i.test(String(err))
? "blocked" : "error";
}
}
The eight-byte module — the magic number and version — validates and instantiates instantly where WebAssembly works. Never infer support from the user agent string; the same browser version can have WebAssembly on or off depending on settings.
Step 2 — decide per feature what “degraded” means
For each Wasm-powered feature, decide in advance what users without WebAssembly get. Three answers cover most cases:
const status = await wasmStatus();
const features = {
imageEditor: status === "ok" ? "full" : "unavailable", // inherently Wasm-heavy
checksum: status === "ok" ? "wasm" : "js-fallback", // small, portable algorithm
search: status === "ok" ? "local" : "server", // can be served from an API instead
};
A JavaScript fallback suits small, portable algorithms, as in shipping a JavaScript fallback for a Wasm feature. A server-side path suits features with an existing backend — search, conversion, validation. And “unavailable, with an explanation” suits features that are inherently local and heavy, such as an in-browser video editor. What does not suit any of them is a blank area or a spinner that never stops.
Step 3 — write the message users see
For features that are unavailable, say so plainly, say why in terms the user can act on, and keep the rest of the page working:
<div class="notice" role="status" hidden id="wasm-off-notice">
<strong>The image editor isn't available in this browser.</strong>
It needs WebAssembly, which is turned off — for example by Lockdown Mode or by your organisation's browser settings.
You can still upload and download images.
</div>
if (features.imageEditor === "unavailable") {
document.getElementById("wasm-off-notice").hidden = false;
document.getElementById("editor").remove();
}
Avoid telling users to “update your browser” — their browser is almost certainly current; WebAssembly was turned off on purpose. Avoid technical language beyond the one term they might need to search for.
Step 4 — record how many users are affected
You cannot prioritise fallbacks without numbers. Report the detection result once per session to your analytics or monitoring:
navigator.sendBeacon("/metrics", JSON.stringify({ event: "wasm_status", status, ua: navigator.userAgent }));
If “blocked” appears in noticeable numbers, look for a CSP problem on your own pages first — that one you can fix. If “absent” is concentrated in one enterprise customer, a server-side path for that customer may be worth building; if it is spread thinly across hardened browsers, a good message may be enough.
Step 5 — test the disabled path
A degraded path that nobody has seen is probably broken. Test it in a real browser with WebAssembly disabled. Chromium-based browsers can start with WebAssembly hidden:
chromium --js-flags="--noexpose-wasm" http://localhost:5173
Firefox can disable it through about:config (javascript.options.wasm set to false), and Safari can be tested in Lockdown Mode on a test
device. Automating the Chromium flag in CI is covered in
testing fallback paths in CI.
Third-party libraries and the degraded path
Your own code is usually the easy part. Libraries that ship WebAssembly — editors, codecs, analytics SDKs, PDF viewers — often load their modules as soon as they are imported, and many do not handle a missing or blocked WebAssembly gracefully. They throw from their initialisation, sometimes from inside a promise nobody awaits, and the error surfaces as an unhandled rejection that stops unrelated code on the page from running.
Isolate such libraries behind the same status check. Import them dynamically only when the status is ok, so a disabled environment never
evaluates their code at all. Wrap their initialisation in a try block that turns failure into your degraded state rather than an uncaught
error. And check whether the library offers a non-Wasm mode — several PDF and image libraries fall back to JavaScript or canvas rendering if
told to — which can turn “unavailable” into “slower” for free.
Finally, keep an eye on what the library does when WebAssembly appears to work but fails later, such as a module that compiles but runs out of memory on a constrained device. Those failures happen after your startup detection has already said yes, so the library’s runtime errors need the same handling: catch them at the feature boundary, show the degraded state, and report the failure.
Thinking about these users
It is tempting to treat users without WebAssembly as an edge case to ignore, and for many applications the numbers are small. But the populations involved are not random. Users in Lockdown Mode are, by definition, people who have reason to fear targeted attacks — journalists, activists, officials. Enterprise users with restricted browsers are often a company’s paying customers, using the product at work. And CSP failures affect everyone on a misconfigured page. A page that degrades well treats each of those groups with respect at very little cost: a detection function, a message, and a test. A page that does not shows them an empty box and a console error they cannot read.
Expected output
With WebAssembly hidden, the page loads, the checksum feature runs on its JavaScript fallback, search uses the server, and the editor area is replaced by the notice. The console shows the detection result rather than an uncaught error:
wasm status: absent → imageEditor: unavailable, checksum: js-fallback, search: server
Gotchas
- Detection that only checks the global. CSP-blocked compilation leaves the global present. Instantiate a tiny module.
- Uncaught rejections from libraries. Third-party code may try to load its module unconditionally. Guard its initialisation with the same status check.
- “Update your browser” messages. Wrong in nearly every case and unhelpful to users who cannot change settings.
- The fallback assumes a JIT. In Lockdown Mode JavaScript runs without a JIT too; a JavaScript fallback that is fast elsewhere may be very slow there. Measure in that environment.
Performance note
The detection costs well under a millisecond where WebAssembly works and slightly more where it throws. In one application, reporting the detection result showed 0.4% of sessions with WebAssembly absent and 2.1% blocked — the latter entirely from a marketing landing page whose CSP had been copied from an older template. Fixing that header recovered the feature for those users in one deploy.
Frequently Asked Questions
Can I ask users to turn WebAssembly on? Rarely appropriate. Lockdown Mode users chose it for good reasons, and enterprise users usually cannot change policy. Offer the degraded path instead.
Does wasm2js help in Lockdown Mode? It runs, because it is plain JavaScript, but without a JIT it is slow. It suits light workloads; see using wasm2js as a fallback.
Should server-rendered content depend on WebAssembly? No. Render the essential content on the server so it appears regardless, and enhance it with Wasm in the browser.
Is WebAssembly ever disabled on mobile by default? Not in mainstream mobile browsers. The cases above are opt-in modes, policies and specialised embeddings.
Related
- Feature detecting Wasm at startup — detection techniques in general.
- Progressive enhancement with Wasm — structuring pages so this is easy.
- Security implications of Wasm in enterprise apps — why organisations restrict it.
- Handling CompileError and LinkError — the errors a blocked compile produces.
← Back to Polyfill Alternatives & Fallbacks