WebGPU Compute Shaders: Practical Setup, Limits, and Common Pitfalls
Learn how to set up a WebGPU compute pipeline, check device limits, avoid common binding and dispatch mistakes, and verify results with a practical vector‑addition example.
13 Dec 2025, 18:37 UTC

Problem
Developers want to run general‑purpose GPU workloads in the browser, but WebGPU’s compute support is still new. A frequent stumbling block is configuring a compute pipeline that works across adapters, respecting device limits, and avoiding mismatched bindings or dispatch sizes that cause silent failures.
Quick Takeaway
To reliably use WebGPU compute shaders:
- Query
device.limitsbefore choosing workgroup sizes or buffer bindings. - Match the shader’s binding indices with the bind group layout exactly.
- Use
dispatchWorkgroupswith counts that stay withinmaxComputeWorkgroupsPerDimensionandmaxComputeWorkgroupSizeX/Y/Z. - Read back the result with
mapAsyncand compare to a CPU reference.
How to Set Up a Compute Pipeline
Below is a minimal, yet complete, example that adds two float32 vectors. The code runs in a modern browser that exposes WebGPU (e.g., Chrome 124+, Firefox 124+). Replace /* … */ placeholders with the actual code snippets when you copy the example.
1. Request a GPU Device
async function init() {
if (!navigator.gpu) {
throw new Error('WebGPU not supported');
}
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
return device;
}
2. Create Buffers
We allocate three buffers: two input buffers and one output buffer. All are GPUBufferUsage.STORAGE and mapped for writing on creation.
function createBuffer(device, data, usage) {
const [buffer, arrayBuffer] = device.createBufferMapped({
size: data.byteLength,
usage,
mappedAtCreation: true,
});
new Float32Array(arrayBuffer).set(data);
buffer.unmap();
return buffer;
}
const N = 1024;
const a = new Float32Array(N).fill(1.0);
const b = new Float32Array(N).fill(2.0);
const inputA = createBuffer(device, a, GPUBufferUsage.STORAGE);
const inputB = createBuffer(device, b, GPUBufferUsage.STORAGE);
const output = device.createBuffer({
size: a.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
3. Write the Shader
The WGSL shader declares three storage buffers and a compute entry point. The @compute @workgroup_size(64) attribute sets a workgroup of 64 threads.
const shaderCode = `
@group(0) @binding(0) var<storage, read> a: array;
@group(0) @binding(1) var<storage, read> b: array;
@group(0) @binding(2) var<storage, write> out: array;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid : vec3) {
let idx = gid.x;
if (idx < arrayLength(&a)) {
out[idx] = a[idx] + b[idx];
}
}
`;
4. Compile Module and Create Pipeline
const module = device.createShaderModule({ code: shaderCode });
const pipeline = device.createComputePipeline({
compute: {
module,
entryPoint: 'main',
},
});
5. Bind Group Layout & Bind Group
Binding indices must match those in the shader. The layout is inferred from the pipeline.
const bindGroupLayout = pipeline.getBindGroupLayout(0);
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: inputA } },
{ binding: 1, resource: { buffer: inputB } },
{ binding: 2, resource: { buffer: output } },
],
});
6. Dispatch Workgroups
The shader’s workgroup size is 64, so we need ceil(N / 64) workgroups. The dispatch must stay within device limits.
const workgroupSize = 64;
const workgroups = Math.ceil(N / workgroupSize);
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(workgroups);
passEncoder.endPass();
const gpuCommands = commandEncoder.finish();
device.queue.submit([gpuCommands]);
7. Read Back the Result
async function readBuffer(device, buffer) {
const readBuffer = device.createBuffer({
size: buffer.size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const commandEncoder = device.createCommandEncoder();
commandEncoder.copyBufferToBuffer(buffer, 0, readBuffer, 0, buffer.size);
device.queue.submit([commandEncoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
const data = new Float32Array(readBuffer.getMappedRange().slice());
readBuffer.unmap();
return data;
}
const result = await readBuffer(device, output);
console.log(result.slice(0, 10)); // should show [3,3,3,…]
Device Limits to Check
Before hard‑coding workgroup sizes or buffer counts, query device.limits. The most relevant fields for compute pipelines are:
| Limit | Description |
|---|---|
| maxComputeWorkgroupSizeX | Maximum X dimension of a workgroup. |
| maxComputeWorkgroupSizeY | Maximum Y dimension of a workgroup. |
| maxComputeWorkgroupSizeZ | Maximum Z dimension of a workgroup. |
| maxComputeWorkgroupsPerDimension | Maximum number of workgroups that can be dispatched in a single dimension. |
| maxUniformBufferBindingSize | Maximum uniform buffer size in bytes. |
Example Check
const limits = device.limits;
console.log('Max workgroup size X:', limits.maxComputeWorkgroupSizeX);
console.log('Max workgroups per dimension:', limits.maxComputeWorkgroupsPerDimension);
Common Pitfalls
- Binding index mismatch: If the shader expects binding 0 but the bind group supplies binding 1, the compute pass will silently fail or produce garbage.
- Wrong entry point name: The
entryPointin the pipeline must match the function name in the shader; otherwisecreateComputePipelinethrows. - Dispatch size out of bounds: Using
dispatchWorkgroups(1000)on a device that only supports 512 per dimension triggers an error. Always compute the maximum allowed dispatch size fromdevice.limits. - Insufficient buffer usage flags: The output buffer must include
GPUBufferUsage.COPY_SRCif you plan to read it back. - Ignoring adapter capabilities: Some adapters do not support storage textures or certain shader features. Use
adapter.featuresto guard against unsupported capabilities.
Verification Checklist
- Run
navigator.gputo confirm API availability. - Log
device.limitsand verify chosen workgroup size and dispatch counts are within limits. - After submission, map the output buffer and compare the first few elements to a CPU‑computed reference.
- Check the browser console for any error messages from
createComputePipelineor dispatch. - If using experimental features, enable the appropriate flag (e.g.,
chrome://flags/#enable-unsafe-webgpu).
Summary
WebGPU compute shaders provide a powerful, low‑level way to harness GPU parallelism in the browser. By querying device limits, matching binding indices, and validating dispatch sizes, developers can avoid the most common implementation errors. The example above demonstrates a full vector addition pipeline from buffer creation to result verification, serving as a template for more complex workloads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.