There is a piece of silicon in your laptop that your web app has never once used. If you bought a machine in the last couple of years, it almost certainly has an NPU: a small, power-efficient accelerator built specifically for running neural networks. Your OS uses it. Native apps use it. The browser, until recently, had no way to reach it at all.
WebGPU got us closer. Running a model through WebGPU means real hardware acceleration instead of grinding through WASM on the CPU, and for a lot of workloads that is the right answer. But WebGPU targets the GPU, which is the part of your machine that is also busy compositing the page, decoding video, and draining the battery. The NPU exists precisely because inference on a GPU is fast but expensive. WebNN is the standard that closes that gap, and it is currently the only web API that provides access to an NPU.
Where the spec actually stands ​
The W3C published an updated Candidate Recommendation of the Web Neural Network API on 22 January 2026, following more than a hundred significant changes since the previous snapshot in April 2024. That is a meaningful signal: the operator set and the graph model have stopped churning enough that browser vendors are being formally invited to implement and test.
Support today is Chromium-only, across ChromeOS, Linux, macOS, Windows, and Android. Chrome and Edge have working implementations. Firefox and Safari do not, and the spec needs two independent implementations passing the test suite before it can advance to a full Recommendation. So this is a progressive-enhancement story, not a "rewrite your inference layer" story. Feature-detect and fall back:
async function pickBackend() {
if (!("ml" in navigator)) return "wasm";
for (const deviceType of ["npu", "gpu"]) {
try {
await navigator.ml.createContext({ deviceType });
return `webnn:${deviceType}`;
} catch {
// context creation throws when that device isn't available
}
}
return "webgpu" in navigator ? "webgpu" : "wasm";
}
The graph model, and why it suits an NPU ​
WebNN is not a tensor library. You do not write a training loop against it. You describe a computational graph once, hand it to the browser to compile, and then execute it repeatedly with different inputs. MLGraphBuilder is the factory that builds that graph:
const context = await navigator.ml.createContext({ deviceType: "npu" });
const builder = new MLGraphBuilder(context);
const descriptor = { dataType: "float32", shape: [1, 3, 224, 224] };
const input = builder.input("input", descriptor);
const weights = builder.constant(
{ dataType: "float32", shape: [32, 3, 3, 3] },
new Float32Array(weightData),
);
const conv = builder.conv2d(input, weights, { padding: [1, 1, 1, 1] });
const output = builder.relu(conv);
const graph = await builder.build({ output });
The build step is where the value lives. Because the browser sees the whole graph before it runs anything, it can fuse operations, pick layouts, and hand the result to whatever accelerator the platform exposes — DirectML on Windows, Core ML on Apple platforms, NNAPI-style paths on Android. That whole-graph view is exactly what NPU drivers want, and it is why an imperative, op-by-op API could never have targeted this hardware well.
You probably want this through ONNX Runtime Web ​
Writing graphs by hand is fine for a demo and miserable for a real model. In practice you reach WebNN through a runtime. ONNX Runtime Web exposes it as an execution provider, so switching backends is a config change:
const session = await ort.InferenceSession.create("./model.onnx", {
executionProviders: [
{ name: "webnn", deviceType: "npu", powerPreference: "default" },
"webgpu",
"wasm",
],
freeDimensionOverrides: { batch: 1, channels: 3, height: 224, width: 224 },
});
Two caveats worth internalizing before you ship this. First, operator coverage: all ONNX operators are supported by the WASM backend, but only a subset are supported by WebGL, WebGPU, and WebNN. Unsupported ops fall back to WASM, which means a model that looks like it is running on the NPU may be silently ping-ponging between backends and performing worse than either pure path. Profile the real model on real hardware; do not trust the backend name in your config. Second, freeDimensionOverrides is not optional decoration — WebNN wants static shapes, and a model with dynamic dimensions will often refuse to compile until you pin them.
Is it worth it yet? ​
If you are already shipping in-browser inference, WebNN is worth wiring in as the first entry in your execution-provider list. The fallback chain costs you a few lines and the win on a machine with a real NPU is not just speed, it is power: inference that does not spin up the GPU and does not tank battery life on a laptop that is not plugged in.
If you are not shipping inference yet, this is not the reason to start. Both the WebGPU and WebNN backends are still described as experimental, coverage is Chromium-only, and the operator gaps are real. But the direction is clear enough that it belongs on your radar. The ONNX Runtime WebNN docs are the fastest way to try it on a model you already have.

