A minimal WebGPU compute shader
Published January 1, 2027
// No @webgpu/types in this repo's tsconfig, so these casts stand in for the
// missing ambient types. Swap them for the real types in a project that has
// @webgpu/types installed.
export const doubleOnGpu = async (input: Float32Array): Promise<Float32Array> => {
const gpu = (navigator as any).gpu;
const { GPUBufferUsage, GPUMapMode } = globalThis as any;
const adapter = await gpu.requestAdapter();
const device = await adapter.requestDevice();
const dataBuffer = device.createBuffer({
size: input.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(dataBuffer, 0, input);
const module = device.createShaderModule({
code: `
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
if (id.x < arrayLength(&data)) {
data[id.x] = data[id.x] * 2.0;
}
}
`,
});
const pipeline = device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "main" } });
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: dataBuffer } }],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(input.length / 64));
pass.end();
const readBuffer = device.createBuffer({
size: input.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
encoder.copyBufferToBuffer(dataBuffer, 0, readBuffer, 0, input.byteLength);
device.queue.submit([encoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(readBuffer.getMappedRange().slice(0));
readBuffer.unmap();
return result;
};What
WebGPU's GPUDevice can run a compute pipeline: a kernel written in WGSL (WebGPU's shading language) dispatched across a grid of GPU threads that each run the same function over a slice of a buffer — general-purpose parallel compute, with no triangle or pixel involved.
Why it matters
WebGL never got compute shaders — it's a rendering API descended from OpenGL ES 2/3, and anything compute-shaped (particle simulation, image filters, physics) had to be smuggled through fragment shaders writing to an off-screen texture, which is exactly as awkward as it sounds. WebGPU is a from-scratch API modeled on Vulkan/Metal/D3D12, and general compute is a first-class citizen alongside rendering: no framebuffer required, just a buffer in, a kernel, a buffer out.
The reason to reach for this at all is throughput. A CPU loop processes an array one element at a time on one core. The snippet's kernel runs the same handful of WGSL lines across thousands of GPU lanes simultaneously — for embarrassingly-parallel, per-element work, that's where the order-of-magnitude speedup actually shows up, not on small arrays where the round trip to the GPU costs more than the computation saves.
How it works
doubleOnGpu doubles every element of a Float32Array on the GPU and reads the result back:
navigator.gpu.requestAdapter()thenadapter.requestDevice()get aGPUDevice— the entry point for everything else. Both can resolve tonull/reject if WebGPU isn't available or no adapter meets requested limits; production code should check.dataBufferis created withSTORAGEusage (so the shader can read/write it),COPY_SRC/COPY_DST(so the CPU can write into it and later copy out of it), thendevice.queue.writeBufferuploads the input.- The WGSL string declares one
@compute @workgroup_size(64)function.@builtin(global_invocation_id)gives each of the dispatched threads its own index; theif (id.x < arrayLength(&data))guard matters becausedispatchWorkgroupsalways dispatches whole workgroups — a 100-element array withworkgroup_size(64)dispatches 2 full workgroups (128 threads), and the extra 28 threads need to no-op rather than touch memory past the buffer's logical length. layout: "auto"oncreateComputePipelineinfers the bind group layout straight from the WGSL — convenient for a single shader, see Gotchas for when that stops being enough.- The compute pass itself is four calls:
setPipeline,setBindGroup,dispatchWorkgroups(Math.ceil(input.length / 64))(one workgroup per 64 elements, rounded up),end(). STORAGEbuffers can't be mapped for reading directly, so the encoder copiesdataBufferinto a separateMAP_READbuffer before submission.mapAsyncthen exposes that buffer's contents as anArrayBuffer;.slice(0)copies it out because the mapped range is invalidated the momentunmap()runs.
Gotchas
- No polyfill exists for missing compute support — feature-detect
navigator.gpuand fall back to a CPU path (or skip the feature) rather than assuming it's there. layout: "auto"can't be shared across multiple pipelines that need the same bind group layout — for anything beyond a one-shader demo, define aGPUBindGroupLayoutexplicitly instead.- This repo's
tsconfig.jsondoesn't include@webgpu/types(WebGPU still isn't part of TypeScript's bundled DOM lib), so the snippet castsnavigatorand readsGPUBufferUsage/GPUMapModeoffglobalThisasanyto typecheck here. Install@webgpu/typesin a real project and drop the casts for full IntelliSense. - GPU work is asynchronous end-to-end but the errors aren't always where you'd expect — a malformed WGSL shader fails at
createShaderModuletime asynchronously via the device's error scopes/uncapturederrorevent, not by throwing synchronously at the call site.
Browser support: Chrome/Edge 113+ (stable since 2023). Safari shipped WebGPU in Safari 26 (macOS Tahoe, iOS/iPadOS 26) in 2026. Firefox is mid-rollout — Windows since Firefox 141, Apple Silicon macOS since Firefox 145 — and not yet on by default everywhere. Treat WebGPU as a progressively-enhanced capability, feature-detected at the call site, not a baseline dependency, until Firefox's rollout finishes.
Related
- WebGL2 — still the right choice for straightforward rendering; it has no compute-shader equivalent.
- Web Workers / WASM SIMD — the CPU-bound parallel option when WebGPU isn't available or the dataset is too small to justify a GPU round trip.