Canvas vs SVG vs WebGL
The browser gives you three native ways to draw: the 2D <canvas> API, SVG, and WebGL. They look similar on screen but work in fundamentally different ways, and picking the wrong one usually means rewriting your rendering layer later. This page contrasts how each model treats your shapes, where each one shines, and how to decide quickly.
The core distinction: immediate vs retained mode
The single most important difference is who remembers your shapes.
Canvas is immediate mode. When you call ctx.fillRect(...), the pixels are painted and the canvas forgets the rectangle ever existed. There are no “objects” afterward — just a grid of pixels. If you want to move that rectangle, you clear the canvas and redraw the whole scene yourself. This makes canvas fast and memory-light for thousands of shapes, but it means hit-testing, accessibility, and “edit this one element” are entirely your responsibility.
SVG is retained mode. Each shape is a real DOM node (<rect>, <circle>, <path>) that lives in the document tree. The browser remembers every element, redraws on its own, and lets you style with CSS, attach event listeners, and animate declaratively. That bookkeeping is powerful but costs memory and layout time, so SVG slows down once you have many thousands of nodes.
WebGL is immediate mode on the GPU. You hand vertex and pixel data to shader programs that run massively in parallel. It is the only option for real 3D and for rendering millions of points at 60fps, but the API is verbose and low-level.
Rule of thumb: a few hundred interactive, styleable elements → SVG. Many thousands of pixels redrawn per frame → canvas. 3D or extreme scale → WebGL.
Decision table
| Concern | 2D Canvas | SVG | WebGL |
|---|---|---|---|
| Rendering model | Immediate (pixels) | Retained (DOM) | Immediate (GPU) |
| Output | Raster bitmap | Vector (scales crisply) | Raster (GPU) |
| Best object count | Thousands+ | Hundreds to low thousands | Millions |
| Resolution independence | No (re-render for DPI) | Yes | No |
| Styling | JS API only | CSS + attributes | Shaders (GLSL) |
| Per-element events / hit-testing | Manual | Built-in (DOM events) | Manual |
| Accessibility | Poor (you add ARIA manually) | Strong (DOM, text, ARIA) | Poor |
| 3D support | No | No | Yes |
| Learning curve | Low | Low | High |
| Typical use | Games, charts, image editing | Icons, diagrams, dashboards | 3D, data-heavy viz, shaders |
When to use each
Reach for SVG when shapes are few, interactive, and need to look perfect at any zoom: logos, icons, flowcharts, form widgets, or a chart where each bar is clickable. Because shapes are DOM nodes, you bind events directly and let CSS handle hover states.
<!DOCTYPE html>
<html>
<head>
<style>
circle { fill: #6366f1; cursor: pointer; transition: fill .2s; }
circle:hover { fill: #f43f5e; }
p { font: 14px sans-serif; }
</style>
</head>
<body>
<svg width="220" height="120" viewBox="0 0 220 120">
<circle id="dot" cx="110" cy="60" r="40" />
</svg>
<p id="status">Hover or click the circle.</p>
<script>
const dot = document.getElementById('dot');
const status = document.getElementById('status');
// Per-element events come for free in SVG — no hit-testing math.
dot.addEventListener('click', () => {
status.textContent = 'Clicked the <circle> DOM node!';
});
</script>
</body>
</html>
Reach for canvas when you redraw a whole scene every frame or manipulate raw pixels: particle effects, 2D games, real-time charts with thousands of points, or image filters. You own the render loop and clear-then-draw each frame.
<!DOCTYPE html>
<html>
<head><style>canvas { border: 1px solid #ddd; }</style></head>
<body>
<canvas id="c" width="320" height="160"></canvas>
<script>
const ctx = document.getElementById('c').getContext('2d');
const dots = Array.from({ length: 400 }, () => ({
x: Math.random() * 320,
y: Math.random() * 160,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
}));
const frame = () => {
// Immediate mode: wipe and repaint the entire scene each tick.
ctx.clearRect(0, 0, 320, 160);
ctx.fillStyle = '#6366f1';
for (const d of dots) {
d.x = (d.x + d.vx + 320) % 320;
d.y = (d.y + d.vy + 160) % 160;
ctx.fillRect(d.x, d.y, 2, 2);
}
requestAnimationFrame(frame);
};
frame();
</script>
</body>
</html>
That 400-dot loop would stutter as 400 SVG <rect> nodes because the browser must reconcile the DOM and recompute layout each frame. With canvas it is just memory writes.
Reach for WebGL when you need true 3D, custom GPU shaders, or are rendering far beyond what the CPU can push — think 3D product viewers, map rendering, or scatter plots with millions of points. Most teams use a wrapper such as Three.js, PixiJS, or regl rather than raw WebGL. Note that the 2D and WebGL contexts are mutually exclusive on a single canvas:
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl2');
if (!gl) {
console.log('WebGL2 unavailable; falling back to 2d canvas');
}
Output:
WebGL2 unavailable; falling back to 2d canvas
Gotcha: you cannot get both a
'2d'and a'webgl'context from the same<canvas>. The firstgetContext()call locks the canvas to that context type for its lifetime. Use separate canvases (or layer them) if you need both.
A note on hybrids and accessibility
These technologies are not exclusive. A common pattern is canvas for a dense data layer with an overlaid transparent SVG (or HTML) layer for crisp, accessible, clickable annotations. Charting libraries like D3 deliberately support both backends for this reason.
If your graphic conveys information, remember that canvas and WebGL are opaque to screen readers — the pixels carry no semantics. You must supply a text alternative, an off-screen data table, or ARIA-described controls. SVG, being part of the DOM, can hold <title>, <desc>, and role attributes that assistive tech reads natively.
Best Practices
- Default to SVG for small, interactive, resolution-independent graphics; you get events, CSS, and accessibility for free.
- Default to canvas once you are redrawing thousands of shapes per frame or touching raw pixel data.
- Reserve WebGL for genuine 3D or extreme scale, and prefer a library (Three.js, PixiJS, regl) over hand-written GLSL.
- Scale canvas backing store by
devicePixelRatioso output stays sharp on high-DPI screens. - Provide a text or ARIA alternative for canvas and WebGL content — they expose no semantics to assistive tech.
- Layer technologies when it helps: canvas for the heavy visuals, SVG/HTML on top for labels and interaction.
- Benchmark with realistic data counts before committing; the right choice changes dramatically between hundreds and millions of objects.