Topic 12.3
WebGPU and WebAssembly Back Ends
In one line
A model in the browser runs on one of two back ends: WebAssembly on the CPU, which almost every browser has, or WebGPU on the graphics chip, which is faster but not everywhere. You detect what the device offers, pick one, and keep a fallback down to "use the server".
Think of it like this
Moving house. You can carry boxes yourself (slow, but anyone can do it, anywhere) or hire a lorry (much faster, but only if a lorry can reach your street and a driver is free). A sensible mover checks first, and carries the boxes by hand if there is no lorry. WebAssembly is carrying by hand. WebGPU is the lorry. Your code checks, then chooses.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Back end
- The engine part that does the computing: WebAssembly on the CPU or WebGPU on the graphics chip.
- WebAssembly (WASM)
- A compact code format that browsers run at near-native speed. Available in all current browsers.
- WebGPU
- A browser API that lets a page use the graphics chip for computing. Faster for models, but not available everywhere.
- Adapter
- The object
requestAdapter()returns to describe a usable GPU.nullmeans none is available. - Secure context
- A page served over HTTPS (or localhost). Many powerful browser features work only there.
- Cross-origin isolated
- A page state, set by two response headers, that unlocks multi-threaded WebAssembly. Check
window.crossOriginIsolated. - Feature detection
- Checking whether the browser has a feature before using it, instead of guessing from the browser's name.
- Execution provider
- ONNX Runtime's name for a back end. Documented names include
wasm,webgpu,webnnandwebgl.
Step by step
01Ask the browser, never guess
Do not decide from the browser's name or version: those lists go out of date and miss flags, settings and blocked hardware. Ask the feature itself. The first live example runs seven real checks on this browser: is navigator.gpu there, is there a GPU adapter, is WebAssembly there, is the page a secure context, is it cross-origin isolated, is the Cache API usable, and does navigator.storage.estimate() work.
**Wrap every check in try … catch.** The preview runs in a sandboxed frame, and in such a frame even the harmless-looking typeof caches throws a SecurityError, and navigator.storage.estimate() rejects with a TypeError. A check that throws is a crashed feature, when it should have been the answer "not usable here". The example's probe() turns a throw into the text threw SecurityError.
The answers appear on the page, not in the console. They depend on your browser and your computer (for example, whether a GPU adapter is found), so the console only gets facts that are the same everywhere: that detection finished without throwing, and the decisions your code makes for given inputs. That is also how you test your own fallback: feed describe() a made-up answer such as "threw SecurityError".
02Run real WebAssembly
The same example builds the 41 bytes of a tiny module by hand, checks them with WebAssembly.validate, creates it with WebAssembly.instantiate, and calls add(20, 22). It also feeds three garbage bytes to instantiate and shows the error name, CompileError. A model engine failing to start looks like that: an error you must catch.
03The GPU check has two steps
The "GPU adapter" row on the page shows one of: no navigator.gpu, a requestAdapter() that gives null, an exception, or a found adapter. Here navigator.gpu exists, because the preview is a secure context, yet on a machine without a usable GPU the adapter is null. All of the first three mean "do not use WebGPU". Only a real adapter means yes.
The example's last lines make the decision testable: describe({ gpu }) is called with four given answers (found, none, threw SecurityError, no navigator.gpu) and the console prints what the app would do for each. The real answer of your browser is never printed to the console, because it would be different on another machine.
04Pick a back end
The second example puts the decision in pickBackend(env) so you can test it on devices you do not own. Order: try WebGPU (with both steps and the try), then WebAssembly, then "none" (the server). It also reports whether multi-threading is possible.
In the real library the choice is one option: device: "webgpu" or device: "wasm". Note the defaults in the library's source: dtype defaults to fp32 on WebGPU and q8 on WebAssembly, so changing the device without setting dtype changes which model file is downloaded.
import { pipeline } from "@huggingface/transformers";
async function hasWebGPU() {
if (!navigator.gpu) return false;
try {
return (await navigator.gpu.requestAdapter()) !== null;
} catch {
return false;
}
}
const device = (await hasWebGPU()) ? "webgpu" : "wasm";
const pipe = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", { device });05Failures after you chose
Detection says what should work, not what will. A GPU can pass the check and still fail when the model is created: not enough memory, a driver problem, or an operation the GPU back end does not support. Wrap the creation in a try, and on failure retry once on WebAssembly before giving up to the server. The Transformers.js documentation itself warns that WebGPU is experimental in many browsers and that a model may fail on WebGPU even if it runs on WebAssembly.
Try it yourself
- 1
Add a retry on failure
Add a helper
createWithFallback(env)to the second example that calls a functionmake(device); ifmake("webgpu")throws, callmake("wasm"). Test it with amakethat always fails onwebgpu.This mirrors what a real app needs: a detection that passes does not guarantee that creation succeeds.
- 2
Look at the isolation answer
In the first example find the page row
cross-origin isolated. It comes fromwindow.crossOriginIsolated.It reads
falsein this preview, so a model engine in a page like this would run on one thread. A site getstrueonly by sending the two isolation headers.
Code & diagrams
Look at the page, not only the console: it lists this browser's real answers, with a throw shown as "threw SecurityError". The console has only facts that are the same on every machine, plus the decisions for four given answers.
Preview
Console
Nothing logged yet.Expected console output
detection finished without throwing: true
module is valid: true
add(20, 22) = 42
garbage is valid: false
CompileError
"found" -> WebGPU is possible
"none" -> GPU exposed but no adapter: use WebAssembly
"threw SecurityError" -> GPU check blocked: use WebAssembly
"no navigator.gpu" -> no WebGPU: use WebAssemblyenv is a made-up description of a device, so we can test devices we do not own. In a real page you would build it from navigator.gpu and WebAssembly.
Preview
Console
Nothing logged yet.Expected console output
desktop with GPU -> webgpu (GPU adapter found)
gpu exposed, no adapter -> wasm (single thread only)
gpu call throws -> wasm (single thread only)
no WebGPU at all -> wasm (single thread only)
locked-down browser -> none (use the server)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Stop at the first check
In the second example change if (adapter) return to return so any exposed navigator.gpu counts as success.
Myth vs fact
Myth
WebGPU is always faster, so always use it.
Fact
It is usually faster for large models, but it needs support, can fail at creation, and may use a different default dtype. For a tiny model the WebAssembly path can be fine and more reliable.
Myth
If navigator.gpu exists, WebGPU works.
Fact
The adapter request can return null or throw, and creating a model can still fail. Check the adapter and keep a fallback.
Myth
Check the browser name instead of the feature.
Fact
Names miss flags, settings and blocked hardware. Feature detection asks the thing you want to use.
Myth
A feature check like typeof caches can never throw.
Fact
In a sandboxed frame (like this page's preview) reading caches throws a SecurityError, and navigator.storage.estimate() rejects. Put every detection inside try … catch and treat a throw as "not usable".
Myth
WebAssembly is slow because it is not JavaScript.
Fact
It is designed to run fast, and it is what most browser model engines use by default. It runs on the CPU, so a big model is slower than on a GPU, but it works almost everywhere.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Detect in the same place the model will run. WebGPU is also available inside workers (
WorkerNavigator.gpu), and a capability can differ between page and worker. Run the check in the worker that will load the model (Topic 12.5). - ▸
Remember the choice, but not for too long. Store "this device failed on WebGPU" so you do not try again on every load, but let it expire, since a browser update may fix it. And log which back end was used, so you can see the real mix of your users.
- ▸
Threads are a site-wide decision. The headers that make a page cross-origin isolated also change how it can load images, scripts and frames from other sites. Test the whole site before turning them on, and do not enable them just for one feature.
Remember this
- 1
The problem this solves: The same model can run well on one device and not at all on another. If your code asks for the graphics chip, it fails on devices without it, and the user sees a broken feature. This topic shows what each back end is, how to check honestly, and how to fall back so every user gets a working feature, even if slower or served by the server.
- 2
WebAssembly (WASM) is a compact binary code format that browsers run at near-native speed on the CPU. It is available in every current browser. Libraries compile their model engines to WASM, so "run on WASM" is the safe default. You can see it work: the first live example builds a real WebAssembly module of 41 bytes with one function,
add, and calls it. A model engine is the same thing, much larger. - 3
WebGPU is a browser API that gives pages access to the graphics chip for general computation, not only for drawing. Models are mostly big sums done in parallel, which graphics chips are built for, so WebGPU can be much faster. But support is limited: the Transformers.js documentation gave about 85 percent of users globally "as of March 2026" (quoting caniuse.com), and says some browsers need a flag or have partial support. When this was checked, caniuse itself listed about 86 percent with full support plus about 2 percent partial (StatCounter data for September 2026). Treat any such figure as a dated estimate, not a fixed fact: it also depends on who your users are. MDN also says it works only in secure contexts (HTTPS pages), and it is available in workers as well.
- 4
Detecting WebGPU takes two steps, and either can say no. First,
navigator.gpumust exist. Many browsers, and every page that is not a secure context, do not have it. Second,await navigator.gpu.requestAdapter()must give you an adapter (a description of a usable GPU). It can givenullwhen no suitable GPU is available, or it can throw. A check that stops at the first step says yes for devices that then fail. - 5
Threads on WASM. The engine can use several threads for speed. ONNX Runtime Web's documentation says multi-threading is enabled only when the browser supports WebAssembly multi-threading and the page is cross-origin isolated (
window.crossOriginIsolatedistrue, which needs two response headers on your page:Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corporcredentialless, as MDN documents). The default thread count is chosen by the runtime (it documents half ofnavigator.hardwareConcurrencyor 4, whichever is smaller), andort.env.wasm.numThreads = 1forces one thread. Isolation changes how your page can embed third-party content, so it is a decision for the whole site. - 6
Choosing is a function of what the device reports. The second live example writes
pickBackend(env)and runs it against five pretend devices, including the awkward ones: a GPU that is exposed but has no adapter, a GPU call that throws, and a locked-down browser with no WebAssembly. The last case returns "use the server", the final fallback.
Explain it without notes
What are WebAssembly and WebGPU, and when would you choose each?
Why is checking navigator.gpu not enough?
How do you fall back when the device cannot run the model at all?
What does cross-origin isolation have to do with running a model?
Practice
Write createWithFallback(make, env) that picks a device with pickBackend, calls make(device), and if that throws on webgpu calls make("wasm"). It should return { device, model } or throw if nothing works.
Add a lowMemory flag to the env object in the second example and make pickBackend return "none" with the note "not enough memory" when it is true.
Trade-offs
- ↔
Supporting both back ends doubles the testing: different default file sizes, different failure modes, different speeds. Supporting only WebAssembly is simpler and works almost everywhere; supporting WebGPU wins on speed for larger models.
- ↔
Turning on cross-origin isolation gives threads, but can break embeds and third-party scripts on your pages. Many teams stay on one thread for small models and accept the speed.
Done when you can
I can describe WebAssembly and WebGPU and what each needs.
I can write a WebGPU check with both steps and error handling.
I can write a
pickBackendfunction that ends in "use the server".I can explain why a successful check still needs a fallback at creation time.
I can say what cross-origin isolation is and why it is a site-wide choice.