Command Palette

Search for a command to run...

Hectal
PHASE 12Advanced ~25 min· topic 3 of 8

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. null means 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, webnn and webgl.

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.

choose.js (real library, not live)whole filejs
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 });
Pick a back enddiagram
Rendering diagram…

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. 1

    Add a retry on failure

    Add a helper createWithFallback(env) to the second example that calls a function make(device); if make("webgpu") throws, call make("wasm"). Test it with a make that always fails on webgpu.

    This mirrors what a real app needs: a detection that passes does not guarantee that creation succeeds.

  2. 2

    Look at the isolation answer

    In the first example find the page row cross-origin isolated. It comes from window.crossOriginIsolated.

    It reads false in this preview, so a model engine in a page like this would run on one thread. A site gets true only by sending the two isolation headers.

Code & diagrams

What this browser really reports, and real WebAssembly

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

Press Run to see the page.

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 WebAssembly
Pick a back end for five different devices

env 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

Press Run to see the page.

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.

terminal
$ The second line of the output
── what you'll see ──
gpu exposed, no adapter -> webgpu (GPU adapter found)

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. 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. 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. 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. 4

    Detecting WebGPU takes two steps, and either can say no. First, navigator.gpu must 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 give null when 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. 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.crossOriginIsolated is true, which needs two response headers on your page: Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp or credentialless, as MDN documents). The default thread count is chosen by the runtime (it documents half of navigator.hardwareConcurrency or 4, whichever is smaller), and ort.env.wasm.numThreads = 1 forces one thread. Isolation changes how your page can embed third-party content, so it is a decision for the whole site.

  6. 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

01

What are WebAssembly and WebGPU, and when would you choose each?

02

Why is checking navigator.gpu not enough?

03

How do you fall back when the device cannot run the model at all?

04

What does cross-origin isolation have to do with running a model?

Practice

01

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.

02

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 pickBackend function 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.