JavaScript WebGPU API Table
| Piece | What it does | Field note |
|---|---|---|
navigator.gpu | The entry gate | undefined means no WebGPU (or an insecure context) - one-line feature detect, WebGL is the honest fallback |
requestAdapter(opts) | The physical pick | resolves null when no usable GPU; powerPreference high-performance vs low-power is a hint, not a command |
requestDevice() | The logical handle | every resource and the command queue hang off it; attach device.lost immediately - it is the rebuild signal |
device.queue.submit() | The command path | record into a GPUCommandEncoder, finish to a command buffer, submit - nothing touches the GPU before submit |
createBuffer + usage flags | The explicit memory | every buffer declares its uses up front (VERTEX, STORAGE, COPY_DST) - wrong usage is a validation error, not corruption |
mapAsync + getMappedRange | The CPU window | JS never sees GPU memory directly: map (await), view as a TypedArray, unmap; readback goes through a staging buffer |
createShaderModule (WGSL) | The shader unit | WGSL is not GLSL; @vertex, @fragment and @compute entry points; getCompilationInfo returns the diagnostics |
device.lost + error scopes | The failure model | fatal resets resolve device.lost; pushErrorScope brackets async suspects; onuncapturederror is the safety net |
WebGPU is the successor to WebGL: instead of exposing a global state machine, it talks to the native graphics APIs underneath (Vulkan, Metal, Direct3D 12) through one explicit interface that works the same on every platform. Everything you do runs through a logical device that you request from a physical adapter, and nothing is implicit - buffers declare what they are for, pipelines declare their layout, and commands are recorded before they run.
Bottom line: the flow never changes, so memorize it once - requestAdapter, requestDevice, configure the canvas context, create resources with declared usages, encode commands into a pass, then queue.submit. WebGPU validates everything up front against those declarations, so a wrong buffer usage or a missing bind group is a clear validation error at the call site, not a black screen. The errors that are not synchronous are async: wrap suspect awaits in pushErrorScope, listen for uncapturederror, and treat device.lost as the rebuild signal.
Compatibility is the first practical question: navigator.gpu exists only on secure contexts, ships in Chrome and Edge, Firefox, and recent Safari - older Safari has it behind flags only. Feature detection is one line (check navigator.gpu, fall back to WebGL), and the two APIs can coexist on one page, so the fallback is real, not theoretical. Compute-only workloads do not even need a canvas: request a device, run a compute pipeline, read the results back through a staging buffer.
How to use
- Feature-detect, then pick an adapter: const adapter = navigator.gpu && await navigator.gpu.requestAdapter({powerPreference: 'high-performance'}); - null means no usable GPU, and that is your cue to fall back to WebGL or a software path.
- Request the device: const device = await adapter.requestDevice(); - every resource (buffers, textures, shader modules, pipelines) and the command queue hang off this one object; attach device.lost immediately so you know when it dies.
- Configure the canvas: const ctx = canvas.getContext('webgpu'); ctx.configure({device, format: navigator.gpu.getPreferredCanvasFormat(), alphaMode: 'premultiplied'}); - getPreferredCanvasFormat avoids the bgra/rgba guesswork across platforms.
- Write shaders as WGSL: device.createShaderModule({code: shaderSource}) with @vertex, @fragment and @compute entry points - WGSL is not GLSL, and getCompilationInfo on the module returns the diagnostics when something does not compile.
- Encode and submit: const enc = device.createCommandEncoder(); begin a render pass on the configured context, setPipeline, setBindGroup, setVertexBuffer, draw, end the pass - then device.queue.submit([enc.finish()]); nothing reaches the GPU until the submit.
Frequently asked questions
Why is WebGPU so much more verbose than WebGL?
Because the explicitness is the product. WebGL hid a global state machine behind convenience: bind this, enable that, and hope the driver sorts it out - which made behavior differ across platforms and made multithreading nearly impossible. WebGPU makes every dependency a declaration: buffers carry usage flags, pipelines carry layouts, passes declare their attachments, and the implementation validates the whole graph before anything runs. The verbosity buys three things: errors that point at the exact declaration instead of a black screen, predictable performance across vendors, and command encoding that does not have to touch the GPU as it happens. The verbosity is front-loaded - you write the declarations once and reuse the pipeline for every frame - and the runtime per-frame code is often shorter than the equivalent WebGL state juggling.
Everything is a promise - how do I actually find my errors?
WebGPU splits errors into three channels, and debugging means using all three. Synchronous validation errors throw or produce error objects at the call site: a wrong usage flag on createBuffer fails right there, in the stack trace you are reading. Async errors need scopes: call device.pushErrorScope('validation'), await the suspect operation, then const err = await device.popErrorScope() - the error, if any, names the object and the rule it broke. Anything that escaped every scope lands in device.onuncapturederror, which is your safety net during development and should log loudly. Fatal problems live on device.lost, a promise that resolves when the device is gone for good (driver reset, GPU process crash, tab throttling policy); the reason carries the classification, and recovery means requesting a fresh device and rebuilding resources - which is why centralizing device creation pays off.
How do I read data back from the GPU, and why is buffer mapping a thing?
GPU memory is not JavaScript memory, and the mapping dance is the border control. A buffer created with MAP_READ or MAP_WRITE usage is not accessible from JS until you map it: call buffer.mapAsync(GPUMapMode.READ), await the promise, then buffer.getMappedRange() hands you an ArrayBuffer view you can copy out - and you must call unmap() before the GPU can touch the buffer again. The catch that shapes real architectures: buffers are also declared with usage flags, and a buffer cannot usually be both a shader-visible storage buffer and a mappable read buffer, so reading compute results back goes through a staging buffer - run the compute into a STORAGE buffer, encoder.copyBufferToBuffer into a COPY_SRC|MAP_READ staging buffer, map that, read, unmap. It is one extra copy and it is the honest price of explicit memory: the GPU and the CPU stop silently fighting over the same bytes.
Can I ship WebGPU today, and what does the fallback look like?
Yes, with a one-line gate and a real fallback path. The gate: if (!navigator.gpu) - and note secure contexts, so http pages and some webviews are out even when the browser ships support. The support picture: Chromium browsers have had it for years, Firefox shipped it, Safari stabilized it recently - old Safari in the wild may still miss it, so the gate matters more than the marketing. The fallback is WebGL, and the two coexist on one canvas-free page or on separate canvases: detect once at startup, pick the renderer, and keep the abstraction at the level of draw-call functions so the WebGL path implements the same interface. Two practical notes from the field: compute-only workloads (no canvas) are the easiest WebGPU-only wins when you can degrade gracefully to a slower JS path, and getPreferredCanvasFormat should choose your canvas texture format - hardcoding rgba8unorm works but can cost a conversion on some platforms.