Offload array math to the GPU with WebGPU compute shaders
WebGPU compute shaders move parallel data work off the browser's main thread, but the explicit setup demands care. Here's how to decide if it's worth it.
13 Oct 2025, 01:49 UTC

A large array on the main thread is a UI problem
Imagine processing two million sensor readings or pixel values with a plain JavaScript loop. Even simple arithmetic over that many floats can block the main thread long enough to drop frames and delay input. Moving that work to the GPU sounds obvious, but WebGPU does not hide the setup behind a convenience function.
What a compute pass actually requires
WebGPU is explicit. You request an adapter, create a device, upload data to a GPU buffer, compile a WGSL shader, create a compute pipeline, bind resources, record commands, and submit them to the queue. Two concepts matter most here: storage buffers, which the shader can read and write, and workgroup size, which controls how many threads run together.
Worked example: scale a Float32Array on the GPU
Run this in a browser console or module on a secure context (localhost or HTTPS). Recent stable Chrome, Edge, and Firefox support WebGPU on most desktop GPUs; mobile support is uneven.
if (!navigator.gpu) {
throw new Error('WebGPU is not available in this browser');
}
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const count = 2_000_000;
const input = new Float32Array(count).fill(1.0);
const gpuBuffer = device.createBuffer({
size: input.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(gpuBuffer, 0, input);
const wgsl = `
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let i = id.x;
if (i < arrayLength(&data)) {
data[i] = data[i] * 2.0;
}
}
`;
const shader = device.createShaderModule({ code: wgsl });
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module: shader, entryPoint: 'main' },
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: gpuBuffer } }],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(count / 64));
pass.end();
device.queue.submit([encoder.finish()]);
const readBuffer = device.createBuffer({
size: input.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(gpuBuffer, 0, readBuffer, 0, input.byteLength);
device.queue.submit([copyEncoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(readBuffer.getMappedRange());
readBuffer.unmap();
console.log(result[0]); // expect 2
console.log(result[count - 1]); // expect 2
Where to run: browser console or a module served over HTTPS or localhost. Required permissions: none beyond a secure context. Risks: requestAdapter can return null on unsupported devices, an incorrect bind group layout can cause silent pipeline creation failures, and dispatchWorkgroups must cover the full array length or trailing elements are skipped.
Trade-offs and limits
- Setup cost: copying data to the GPU and reading it back can take longer than the computation itself for small arrays.
- Mobile support: many phones still lack stable WebGPU, so a CPU fallback is necessary for broad reach.
- Debugging: shader compilation errors and out-of-bounds access are harder to trace than JavaScript exceptions.
- Memory: buffers must be created with the correct usage flags; omitting GPUBufferUsage.STORAGE makes the buffer unreadable by the shader.
Check the result before you commit
- Open DevTools on your page and run
!!navigator.gpu; expecttrue. - Time a plain JavaScript loop and the GPU path with
performance.now()over several runs, including upload and readback. - Inspect the mapped
resultbuffer; every value should equal the shader's output (for example,2in the sample above). - If
requestAdapter()returns null, WebGPU is disabled or unsupported on that device; fall back to CPU or WebGL.
When to use it
Use WebGPU compute when the dataset is large enough to amortize the upload and readback cost, and the operation is embarrassingly parallel. For small arrays or one-off math, a typed array loop on the CPU remains the simpler choice.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.