WebGL & WebGPU
Past SVG and Canvas 2D lies the GPU: WebGL and WebGPU. Learn the rendering ladder, when each rung is worth it, and the accessibility fallback you always owe.
In one line: start at the simplest layer that can draw your thing — CSS, then SVG, then Canvas 2D — and only climb to WebGL or WebGPU when the GPU is genuinely the only rung that fits, because every step up trades accessibility and simplicity for raw power.
What it is
Web graphics live on a ladder, cheapest and most accessible rung first:
- CSS — boxes, gradients, transforms, and compositor-accelerated motion, all declarative.
- SVG — a retained-mode vector tree that is DOM: each shape is a node you can style, inspect, and label.
- Canvas 2D — an immediate-mode pixel surface: you issue draw calls each frame and the browser keeps no scene graph for you.
- WebGL — a low-level GPU API that rasterizes triangles through programmable shaders (GLSL).
- WebGPU — the modern successor: a cleaner API surface, explicit pipelines, and compute shaders for general-purpose GPU work.
The line that matters runs through the middle. CSS and SVG are retained-mode: the browser holds a tree of objects, repaints when it changes, and exposes that tree to assistive technology. Canvas 2D, WebGL, and WebGPU are immediate-mode: you push pixels and triangles to the GPU, nothing is retained as a queryable node, and the result is opaque to everything but the eye.
At a high level the GPU pipeline takes vertex data, runs a vertex shader to position points in clip space, assembles them into triangles, rasterizes those into fragments, and runs a fragment shader to color each pixel. WebGL and WebGPU are two generations of API over that same pipeline; WebGPU adds compute shaders that run general parallel work with no triangles involved at all.
Why it matters
Each rung up buys you something real — and charges for it. SVG over CSS buys arbitrary vector shapes and data binding, at the cost of a DOM node per shape that gets expensive past a few thousand. Canvas 2D over SVG buys thousands of sprites per frame from a single element, at the cost of losing the DOM entirely: no nodes, no built-in hit-testing, no accessibility. WebGL over Canvas 2D buys per-pixel GPU effects and real 3D, at the cost of shader code, context-loss handling, and a steep learning curve. WebGPU over WebGL buys compute and a modern API, at the cost of being the newest rung — you must feature-detect and ship a fallback.
So the cost of climbing is always the same three things: accessibility (pixels can't be read by a screen reader), bundle and complexity (a 3D engine is hundreds of kilobytes and a new mental model), and fragility (GPU contexts can be lost, and not every device or browser can run the top rungs). The skill is refusing to climb until the rung you're on genuinely can't do the job.
See it
Tweak it3
Pick what you're drawing — a static icon, simple shapes, many sprites, a photo filter, a 3D scene, or GPU compute — and the panel names the recommended rendering layer, a one-line reason, the main tradeoff, and the accessibility fallback you owe once you leave the DOM.
How it works
A fair profile of the three GPU-adjacent rungs:
Canvas 2D — an immediate-mode pixel surface. You get a 2d context and issue drawing commands (fillRect, drawImage, arc) every frame; the canvas keeps no scene graph, so animation means clearing and redrawing. Strength: one element draws thousands of shapes per frame with no DOM cost. Tradeoff: you own everything the DOM used to give you — layout, hit-testing, and accessibility.
WebGL — the GPU's triangles, exposed to the browser. You upload vertex buffers, write a vertex and a fragment shader in GLSL, and the GPU rasterizes. Almost no one writes raw WebGL for long; libraries like three.js (full 3D scenes), regl (functional WebGL), and PixiJS (2D at GPU speed) wrap it. Strength: real-time per-pixel effects and genuine 3D. Tradeoff: shader complexity, context-loss handling, and a real learning curve.
WebGPU — the modern API. Explicit render and compute pipelines, a cleaner resource model, and compute shaders for massive parallel work (particles, simulation, ML) with no triangles required. Strength: compute plus a forward-looking API. Tradeoff: the newest rung — you must feature-detect navigator.gpu and fall back to WebGL or Canvas where it's missing.
Build it
Detect the top rung first and degrade gracefully. WebGPU is feature-detected on navigator.gpu; a WebGL context can return null, so guard it. Keep any GLSL as a plain string so the snippet stays valid JavaScript:
async function pickRenderer(canvas) {
// Top rung: WebGPU. Always feature-detect — it isn't everywhere yet.
if (navigator.gpu) {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
const device = await adapter.requestDevice();
return { kind: "webgpu", device };
}
// gpu exists but no adapter (e.g. blocklisted) — fall through.
}
// Next rung down: WebGL. getContext can return null, so guard it.
const gl = canvas.getContext("webgl");
if (!gl) {
// Final fallback: Canvas 2D, or an accessible DOM/SVG representation.
const ctx = canvas.getContext("2d");
return { kind: "canvas2d", ctx };
}
// A fragment shader is just source text from JS's point of view, so we
// keep GLSL in a template string — the file stays valid JavaScript.
const fragmentSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(0.2, 0.6, 1.0, 1.0);
}
`;
const shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shader, fragmentSource);
gl.compileShader(shader);
return { kind: "webgl", gl, shader };
}Make it yours
Use the controls beside the demo above to change what you're drawing, emphasize, and show full layer ladder — each change updates the example live.
Experiment in the playground- Walk every need from top to bottom and notice how often the answer is not the GPU — static icons and simple charts both stay on SVG, where accessibility is free.
- For each GPU pick, read the a11y line: it names the parallel DOM representation you owe, which is the part most demos skip.
- Take a real graphic from your own product and run it through the matrix. If it lands on SVG or Canvas 2D, you can delete the 3D engine you were about to add.
Reproduce it with an LLM
Reproduce it with an LLM
You are a graphics-savvy front-end architect. I need to build '{an interactive scator plot with 50,000 animated points}'. Recommend the right rendering layer from this ladder — CSS, SVG, Canvas 2D, WebGL (e.g. via three.js/regl/PixiJS), or WebGPU — and justify it: why the layers below it run out of headroom, the rough performance characteristics, the accessibility cost (canvas/WebGL output is invisible to assistive tech — describe the fallback you'd provide: a data table, ARIA, or an SVG layer), the bundle/complexity tradeoff, and the progressive-enhancement story (WebGPU feature-detection with a WebGL or Canvas fallback). Be honest about when the simpler layer is the right answer. Return a short recommendation brief.
Pitfalls & accessibility
- Treat every GPU context as fragile: handle
webglcontextlost, feature-detectnavigator.gpubefore use, and make sure the experience still works — or degrades to a static image — when the top rung isn't available.