Canvas, workers & graphics
Canvas and graphics-heavy UIs require a different model from DOM apps. The browser does not retain semantic elements for each shape you draw. You own the scene model, redraw strategy, input mapping, accessibility fallback, and performance budget.
Core details
| Concept | Meaning | Risk |
|---|---|---|
| Immediate mode | Canvas draws pixels now; it does not remember objects | must redraw after every change |
| Retained scene model | your app stores shapes/entities separately | bugs when pixels and model drift |
| Render loop | usually driven by requestAnimationFrame | work can exceed frame budget |
| Coordinate space | CSS pixels, device pixels, transforms, camera | blurry or incorrect hit testing |
| Hit testing | map pointer to scene object | expensive search or wrong transforms |
| OffscreenCanvas | render in a Worker when supported | transfer and messaging overhead |
| Accessibility | canvas has no built-in semantic tree | need DOM controls, labels, summaries, keyboard path |
Canvas contexts: 2d for immediate drawing, WebGL/WebGPU for GPU pipelines, and bitmap/offscreen APIs for specialized workloads. Choose based on workload, team expertise, browser support, and debugging cost.
requestAnimationFrame: schedule visual updates before paint. It is one-shot, so continuous animation must request the next frame. Avoid setInterval for animation timing.
Workers: use Workers for CPU-heavy parsing, simulation, image processing, geometry, compression, or OffscreenCanvas rendering. Workers cannot mutate DOM directly.
Understanding
Canvas is fast when you control the amount of drawing and memory. It becomes slow when every frame redraws too much, allocates objects, decodes images, performs expensive hit testing, or copies large payloads between threads.
The first architecture decision is whether the UI is truly graphics-first. If users need selectable text, forms, links, accessible controls, and layout, the DOM may be the primary surface with small canvas regions. If the product is a map, editor, waveform, game, charting engine, or whiteboard, a retained scene model plus canvas renderer can make sense.
High-DPI rendering matters. A canvas styled at 500 by 300 CSS pixels may need a 1000 by 600 backing store on a 2x screen. Without scaling, graphics look blurry; with careless scaling, memory doubles in each dimension.
Practical examples
High-DPI setup:
function resizeCanvas(canvas: HTMLCanvasElement) {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
const ctx = canvas.getContext("2d")!;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return ctx;
}Render loop shape:
let rafId = 0;
function frame(time: number) {
updateSimulation(time);
drawScene();
rafId = requestAnimationFrame(frame);
}
rafId = requestAnimationFrame(frame);Architecture choices:
| Need | Good approach |
|---|---|
| Static chart | SVG or DOM may be simpler and accessible |
| Thousands of moving points | Canvas/WebGL with spatial index |
| Text editing | DOM overlay or specialized editor model |
| Heavy image processing | Worker + OffscreenCanvas where supported |
| Whiteboard | retained scene model + dirty-region redraw |
Senior understanding
| Probe | Strong answer |
|---|---|
| “Canvas vs SVG?” | Canvas for many pixels/objects; SVG/DOM for semantics and inspectable shapes |
| “Why Worker?” | Move CPU/rendering off main thread, but account for transfer cost |
| “Hit testing?” | Use scene graph, transforms, spatial index, and pointer capture |
| “Accessibility?” | Provide keyboard/DOM equivalents, labels, summaries, and non-visual workflows |
Failure modes
- Redrawing the full scene every frame when only a small region changed.
- Allocating new objects in hot draw loops and creating GC jank.
- Ignoring device pixel ratio and producing blurry output.
- Copying huge arrays between Worker and main thread every frame.
- Building an inaccessible canvas-only control for form-like UI.
Diagram
See also
Spotted something unclear or wrong on this page?