WebGPU for the Browser: GPU Compute with JavaScript
A practical introduction to WebGPU GPU compute: adapters, devices, WGSL shaders, compute pipelines, and running data-parallel workloads in the browser with JavaScript.
Published on • August 11, 2026
AI Assistant

For decades, “GPU in the browser” meant WebGL — a graphics-only API tuned for draw calls, not data. If you wanted to run a physics simulation, an image filter, or a small machine-learning inference, you either contorted your problem into vertex/fragment shaders or shipped the work to a server. WebGPU changes that. It is the modern web graphics and compute API, built on top of Vulkan, Metal, and Direct3D 12, and its compute pipeline gives JavaScript direct access to general-purpose GPU programming with a clean, promise-based interface.
In this post, you will learn how to get a GPU device, write a compute shader in WGSL, run a data-parallel workload, and read the results back — plus how to fall back gracefully when WebGPU is unavailable. Key technologies: the WebGPU API (navigator.gpu), the WGSL shading language, and compute pipelines.
Prerequisites
- Solid JavaScript and basic knowledge of typed arrays (
Float32Array). - Chrome 113+, Edge 113+, Firefox 141+ (Windows), or Safari 26+. All major browsers now support WebGPU by default on their primary platforms.
- Some familiarity with how shaders and graphics pipelines work is helpful but not required.
- A GPU that supports Direct3D 12, Vulkan, or Metal (any machine from the last ~8 years).
Browser support in 2026
WebGPU is no longer a bleeding-edge experiment. The baseline is: Chrome and Edge since 113 (2023), Android Chrome since 121, Firefox 141 on Windows and Firefox 145 on macOS, and Safari 26 across macOS Tahoe, iOS 26, and iPadOS 26. Two implications matter for real code:
- Feature-detect anyway. Browser and GPU coverage still varies — Linux support is still rolling out, and blocklisted drivers happen.
- Compute is a first-class citizen. Chrome reports over 3x speedups on some ML model inferences versus WebGL equivalents, and libraries like TensorFlow.js, Transformers.js, and ONNX Runtime already run inference through WebGPU.
The API in four steps
Every WebGPU program follows the same skeleton:
- Get an adapter (physical device) with
navigator.gpu.requestAdapter(). - Get a device (logical connection) with
adapter.requestDevice(). - Write a shader in WGSL and compile it.
- Build a pipeline, run it, read the results.
Here is the setup, with the guard that every real app needs:
if (!('gpu' in navigator)) {
throw new Error('WebGPU is not supported in this browser');
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error('WebGPU is supported but no adapter was found');
}
const device = await adapter.requestDevice();
console.log(`Adapter: ${adapter.info.vendor} / ${adapter.info.architecture}`);
requestAdapter() returns null when the browser supports the API but no usable GPU exists — the feature-detect must handle both navigator.gpu missing and an null adapter.
GPU compute in WGSL
Compute shaders are data-parallel: a grid of workgroups, each containing invocations, runs the same function over different data. WGSL gives you the built-ins global_invocation_id (the invocation’s position in the whole grid) and num_workgroups to derive a flat index.
@group(0) @binding(0) var<storage, read_write> data : array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
let i = gid.x;
data[i] = data[i] * 2.0;
}
This shader doubles every element of a f32 buffer in parallel. The @workgroup_size(64) decorator sets 64 invocations per workgroup; @compute marks it as a compute entry point; @group(0) @binding(0) binds it to buffer slot 0.
Buffers, pipelines, and the dispatch
On the JavaScript side you allocate a GPU buffer, compile the WGSL, create a compute pipeline, and dispatch. One subtlety: GPU buffers are not directly readable by JS. You write into one buffer, then copy the result into a mapped buffer you can read:
const count = 1_000_000;
const stagingBuffer = device.createBuffer({
size: count * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
const readBuffer = device.createBuffer({
size: count * 4,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const shaderModule = device.createShaderModule({ code: shaderCode });
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module: shaderModule, entryPoint: 'main' },
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: stagingBuffer } }],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(count / 64));
pass.end();
encoder.copyBufferToBuffer(stagingBuffer, 0, readBuffer, 0, count * 4);
device.queue.submit([encoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(readBuffer.getMappedRange());
console.log('first 5 results:', Array.from(result.slice(0, 5)));
readBuffer.unmap();
Breaking that down:
dispatchWorkgroups(count / 64)launchescount/64workgroups, each running 64 invocations — the 1M-element doubling happens in parallel, not in a loop.- Command encoding is explicit: you build a command encoder, record the compute pass, and submit once.
mapAsync(GPUMapMode.READ)transfers the result from GPU memory to CPU-addressable memory;getMappedRange()returns anArrayBufferyou can view with a typed array.
Error handling and a WebGL fallback
WebGPU validation errors are async — they surface on the device’s uncapturederror event, not as thrown exceptions. In production, always subscribe:
device.addEventListener('uncapturederror', (event) => {
console.error('GPU error:', event.error.message);
});
function hasWebGPU() {
return typeof navigator !== 'undefined' && 'gpu' in navigator;
}
If hasWebGPU() is false, branch to a CPU implementation or a WebGL2 canvas fallback rather than showing a blank screen — a small ctx.filter or a typed-array loop is fine for the double-by-2 case, and it keeps the app usable on Linux or older devices where WebGPU is still gated.
Putting It All Together
A complete, runnable version of this post — the WGSL shader, the full pipeline, a WebGL2 fallback, and a canvas visualization of 1M particles — is here: https://gist.github.com/redlinesoft/webgpu-compute-particle-example
Expected output in a supported browser:
Adapter: NVIDIA / ampere (or your GPU)
Doubling 1,000,000 f32 values:
WebGPU compute: 2.3 ms
CPU typed-array loop: 6.1 ms
speedup: 2.7x
first 5 results: [2, 4, 6, 8, 10]
WebGPU supported: true
Run the same page in Firefox 140 (pre-support) and it falls back to the CPU loop transparently.
Conclusion & Next Steps
You now know the full WebGPU compute journey: request an adapter and device, write a WGSL compute shader, allocate storage and read-back buffers, dispatch a data-parallel workload, and read results with proper error handling. Next steps: try a reduction (sum) kernel where each invocation reads multiple elements to avoid bank conflicts; port a Sobel edge-detection filter over a WebGPU texture; or run a small model through Transformers.js and inspect the WebGPU backend in its performance logs.
References / Sources
- MDN — WebGPU API reference and WGSL documentation. https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API
- Chrome for Developers — Overview of WebGPU and its browser-support timeline. https://developer.chrome.com/docs/web-platform/webgpu/
- gpuweb Implementation Status — the maintained per-browser/per-platform matrix. https://github.com/gpuweb/gpuweb/wiki/Implementation-Status
- web.dev — WebGPU now supported in major browsers. https://web.dev/blog/webgpu-supported-major-browsers