Browser API · WGSL
WebGPU
The successor. Stricter, far more capable, and finally shipping.
The reference scene
Checking for WebGPU…
Not running in this browser
What it is
WebGPU is the modern replacement for WebGL, modelled on Vulkan, Metal and D3D12 rather than on 2008-era OpenGL. It brings compute shaders, storage buffers, render bundles and an explicit pipeline model, and it uses its own shading language, WGSL, instead of GLSL.
The setup is longer than WebGL and much more explicit — you describe a pipeline object up front instead of mutating global state per draw. In exchange, validation happens once at pipeline creation with real error messages, rather than as a silent black screen at draw time.
Reach for it when
- You need compute: simulation, particles, image processing, ML inference on the GPU.
- You are drawing enough to care about CPU overhead — the explicit model is dramatically cheaper per draw.
- You want shader errors that name the line rather than a blank canvas.
Look elsewhere when
- You must support older devices. Availability is good in current browsers and absent in old ones, so you need a WebGL fallback or an honest message.
- You only ever draw a couple of things. The extra ceremony buys you nothing.
The code
The same plasma, written the way WebGPU wants it written.
struct Uniforms { time: f32 };
@group(0) @binding(0) var<uniform> u: Uniforms;
@fragment
fn fs(@location(0) uv: vec2f) -> @location(0) vec4f {
// Identical maths to the GLSL version, different spelling.
let colour = 0.5 + 0.5 * cos(u.time + vec3f(uv, uv.x) + vec3f(0.0, 2.0, 4.0));
return vec4f(colour, 1.0);
}const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'opaque' });
const shaderModule = device.createShaderModule({ code: shaderSource });
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module: shaderModule, entryPoint: 'vs' },
fragment: { module: shaderModule, entryPoint: 'fs', targets: [{ format }] },
primitive: { topology: 'triangle-list' },
});
const uniforms = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniforms } }],
});
function frame(now) {
device.queue.writeBuffer(uniforms, 0, new Float32Array([now / 1000]));
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}Notice how much is decided once, up front: the pipeline knows its shaders, formats and layout before a single frame is drawn.
Things that will catch you
- Everything is async, and adapters can be null
- requestAdapter() returns null on unsupported hardware rather than throwing. Check it, or you will read a property of null on exactly the machines you cannot reproduce on.
- Uniform buffers are padded to 16 bytes
- A single f32 uniform still needs a 16-byte buffer. WGSL struct layout follows strict alignment rules, and getting them wrong gives you plausible-looking garbage rather than an error.
- It is not a drop-in for WebGL
- Y in clip space, the depth range, and texture origin conventions all differ. Porting a WebGL renderer is a rewrite of the plumbing, not a search and replace.
Where to learn it
These and 35 more on the reading path.