Every chart on the web is eventually drawn by one of three technologies: SVG, the Canvas 2D API or WebGL (with WebGPU now arriving as its successor). They can produce visually identical results, yet they behave completely differently once you add thousands of data points, hover tooltips, animation, screen readers or a phone with a modest GPU. Choosing the wrong one is rarely fatal on day one, but it often becomes painful when the dataset grows or the product team asks for “just one more interaction”. This guide explains how each renderer works, where it shines, where it breaks down and how we decide between them on real projects.
Two mental models: retained and immediate mode#
The most important difference is not speed but the programming model.
SVG is retained mode. You describe a scene as a tree of elements — <rect>, <circle>, <path>, <text> — and the browser remembers it. Change an attribute and the browser works out what to repaint. Each mark is a real DOM node with its own events, styles and accessibility information.
Canvas and WebGL are immediate mode. You issue drawing commands and the browser turns them into pixels in a bitmap. Once drawn, there are no “objects” left — only colored pixels. To change anything, you clear and redraw. To know what the user is pointing at, you have to work it out yourself from your own data.
WebGL takes the immediate model further by moving the work to the GPU: you upload data into buffers once and run small programs called shaders that position and color every vertex and pixel in parallel. That is why it handles orders of magnitude more marks, and also why it demands considerably more code and expertise.
SVG: when the DOM is your friend#
For the majority of business charts — bar charts, line charts with a few series, donut charts, small multiples, annotated explanatory graphics — SVG is the right default.
Strengths#
- Resolution independence. Vectors stay crisp at any zoom level and on any pixel density, with no extra work.
- Styling with CSS. Colors, hover states, transitions, dark mode and print styles can all live in your stylesheet.
- Native interaction. Every mark can receive pointer and focus events, so tooltips and highlighting are straightforward.
- Accessibility and text. SVG text is selectable, searchable and can be exposed to assistive technology through roles, titles and descriptions.
- Debuggability. You can inspect any bar in devtools and see exactly what was rendered.
An accessible SVG chart#
Here is a deliberately small, hand-written bar chart. Note the role="img" with a title and description that state the insight, not just the chart type:
<figure>
<svg viewBox="0 0 320 170" role="img"
aria-labelledby="orders-title orders-desc">
<title id="orders-title">Monthly orders, January to March</title>
<desc id="orders-desc">Orders rose from 120 in January to 180 in March.</desc>
<g class="bars">
<rect x="20" y="100" width="60" height="60"></rect>
<rect x="130" y="85" width="60" height="75"></rect>
<rect x="240" y="70" width="60" height="90"></rect>
</g>
<g class="labels" aria-hidden="true">
<text x="50" y="168" text-anchor="middle">Jan</text>
<text x="160" y="168" text-anchor="middle">Feb</text>
<text x="270" y="168" text-anchor="middle">Mar</text>
</g>
</svg>
<figcaption>Orders per month, Q1. Full data in the table below.</figcaption>
</figure>
Where SVG struggles#
Because every mark is a DOM node, cost grows with the number of marks: memory, style recalculation, layout and painting. A few hundred or a couple of thousand elements are typically no problem. Tens of thousands of animated circles will make most devices stutter, and hover handlers on each of them compound the problem. There is no fixed threshold — it depends on the device, the complexity of each shape and how often you update — so treat any number you read, including ours, as a rule of thumb and test on your slowest target device.
A common trick is to keep SVG but reduce the node count: draw a 10,000-point line as a single <path> rather than 10,000 circles, and only render individual dots for the hovered point.
Canvas 2D: pixels on demand#
The Canvas API gives you a bitmap and a set of drawing commands. Its cost depends on how many pixels and draw calls you produce, not on how many objects the browser must track. That makes it a strong choice for dense scatter plots, heatmaps, long time series, real-time streams and any chart that redraws many times per second.
Handle pixel density yourself#
A canvas has two sizes: its CSS display size and its internal bitmap size. On a high-density screen, a canvas whose bitmap matches its CSS size looks blurry. Scale the backing store by devicePixelRatio and then scale the context so you can keep drawing in CSS pixels:
function setupCanvas(canvas) {
const dpr = window.devicePixelRatio || 1;
const { width, height } = canvas.getBoundingClientRect();
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return { ctx, width, height };
}
function drawPoints(ctx, points, xScale, yScale) {
ctx.fillStyle = '#2563eb';
ctx.beginPath(); // one path, one fill: far fewer draw calls
for (const p of points) {
const x = xScale(p.x);
const y = yScale(p.y);
ctx.moveTo(x + 2, y);
ctx.arc(x, y, 2, 0, Math.PI * 2);
}
ctx.fill();
}
Batching all points of the same style into one path, as above, is one of the biggest Canvas performance wins. Changing fillStyle, line width or shadows between every mark forces far more state changes.
Hit testing#
Without DOM nodes, hover and click require you to map a pointer position back to data. Options, from simplest to most scalable:
- Arithmetic for regular layouts: a bar chart can compute the hovered bar index from the x coordinate.
Path2DwithisPointInPath()for a modest number of irregular shapes, such as map regions.- A spatial index such as a quadtree or grid for large point sets, so the nearest point is found without scanning everything.
import { quadtree } from 'd3-quadtree';
const tree = quadtree()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.addAll(points);
canvas.addEventListener('pointermove', (event) => {
const rect = canvas.getBoundingClientRect();
const hit = tree.find(event.clientX - rect.left, event.clientY - rect.top, 8);
showTooltip(hit); // hit is undefined when nothing is within 8px
});
Layering#
Stack several canvases (or a canvas under an SVG) so that static content — axes, gridlines, thousands of data points — is drawn once, while a thin top layer handles frequently changing elements such as the crosshair, selection or tooltip. Redrawing a nearly empty overlay is almost free.
WebGL and WebGPU: the GPU route#
WebGL exposes the GPU through an OpenGL ES–style API. Instead of looping over points in JavaScript every frame, you upload coordinates to a buffer once and let a vertex shader transform them in parallel:
attribute vec2 a_position; // data coordinates
uniform vec2 u_scale; // data-to-clip-space scale
uniform vec2 u_offset; // pan offset
uniform float u_pointSize;
void main() {
gl_Position = vec4(a_position * u_scale + u_offset, 0.0, 1.0);
gl_PointSize = u_pointSize;
}
Panning and zooming then only means updating two uniforms, even for a million points. That is the kind of workload — dense scatter plots, large time series, flow fields, geospatial layers, network graphs, 3D — where WebGL is the right tool.
The costs#
- Complexity. Shaders, buffers, blending and precision issues require specialist knowledge. Most teams should use a library rather than raw WebGL.
- Text. WebGL has no text rendering. Labels are usually drawn with signed-distance-field fonts or, more pragmatically, as HTML or SVG overlays.
- Context limits and loss. Browsers cap the number of simultaneously active WebGL contexts, so a dashboard with twenty WebGL widgets can lose some. Contexts can also be lost when the GPU resets, and you must handle the
webglcontextlostandwebglcontextrestoredevents. - Battery and heat. Continuous GPU rendering drains mobile batteries. Render on demand, not in an endless loop.
Where WebGPU fits#
The WebGPU API is the modern successor, with a cleaner design and compute shaders that can aggregate, filter or lay out data directly on the GPU. It has shipped in Chromium-based browsers and more recently in other engines on some platforms, but MDN still marks it as not Baseline. Treat it as a progressive enhancement: feature-detect navigator.gpu and fall back to WebGL, which many rendering libraries now do for you.
Side-by-side comparison#
The figures below are rough orders of magnitude for smooth interaction on mid-range hardware, not benchmarks; your mileage will vary with shape complexity, update frequency and device.
| SVG | Canvas 2D | WebGL / WebGPU | |
|---|---|---|---|
| Model | Retained (DOM nodes) | Immediate (bitmap) | Immediate (GPU buffers and shaders) |
| Comfortable mark count | Hundreds to low thousands | Thousands to hundreds of thousands | Hundreds of thousands to millions |
| Interaction and hit testing | Built in, per element | Manual (math, Path2D, spatial index) | Manual (spatial index, GPU picking) |
| Styling | CSS | JavaScript drawing state | Shaders |
| Text and labels | Excellent | Good, not selectable | None natively; use overlays |
| Accessibility | Can be exposed directly | Requires fallback content and alternatives | Requires fallback content and alternatives |
| Sharpness when zoomed | Always crisp | Must redraw at new scale | Must redraw at new scale |
| Development effort | Low | Medium | High |
| Typical uses | Business charts, explanatory graphics, maps with few regions | Dense plots, heatmaps, real-time streams, animation | Big data scatter, geospatial, 3D, particle-heavy visuals |
Performance techniques that apply to all three#
The fastest mark is the one you never draw. Before switching renderers, reduce the work.
Reduce the data#
A line chart 800 pixels wide cannot show more than a few thousand meaningful points. Downsample long series with an algorithm that preserves visual shape, such as min/max per pixel column or Largest-Triangle-Three-Buckets, and fetch more detail only when the user zooms in. Aggregate dense scatter plots into hexbins or density grids. Often this lets you stay with SVG and keep all its advantages.
Move work off the main thread#
Parsing large JSON, computing aggregates and laying out graphs can block input and hurt Interaction to Next Paint. Do that work in a Web Worker. With OffscreenCanvas, you can even render a Canvas or WebGL chart entirely inside a worker:
// main.js
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('chart-worker.js');
worker.postMessage({ canvas: offscreen, dpr: devicePixelRatio }, [offscreen]);
// chart-worker.js
self.onmessage = ({ data }) => {
const ctx = data.canvas.getContext('2d');
ctx.scale(data.dpr, data.dpr);
// ...fetch, aggregate and draw without touching the main thread
};
Render on demand#
Redraw only when data, size or interaction state changes, and coalesce multiple changes into a single requestAnimationFrame callback. A chart that idles at zero CPU is better for battery life and for every other script on the page. If you want to see this pattern applied to continuous animation, our Canvas particle animation tutorial walks through a frame loop, delta time and device pixel ratio handling in detail.
Mind page weight#
A charting library can easily outweigh the rest of your page. Import only the modules you use, lazy-load charts below the fold and consider whether a static SVG generated on the server would answer the question just as well. Our article on modern JavaScript without the bloat covers these trade-offs more broadly.
Accessible charts, whatever the renderer#
A chart is a way of communicating data, and some of your readers will not see it. Regardless of the technology:
- State the insight in text. A title such as “Orders grew 50% in Q1” plus a short description serves everyone, including people skimming.
- Provide the data. An HTML table, which can be visually collapsed behind a “Show data” toggle, or a CSV download lets screen reader users and analysts explore the numbers.
- Do not rely on color alone. Use direct labels, patterns or shapes so series remain distinguishable for people with color vision deficiencies, and keep graphical elements at a 3:1 contrast ratio against adjacent colors as WCAG’s non-text contrast criterion requires.
- Make interactions keyboard-operable. If tooltips reveal values on hover, offer a way to reach the same information by keyboard.
- Use canvas fallback content. Content placed between
<canvas>and</canvas>is exposed to assistive technology and is a good place for a summary or link to the data table. - Respect reduced motion. Skip entrance animations and animated transitions when
prefers-reduced-motion: reduceis set.
Our WCAG 2.2 guide explains the relevant success criteria in plain language.
Choosing a renderer and a library#
In practice you will rarely choose a renderer in isolation; you will choose a library that uses one or more of them. Low-level toolkits such as D3 let you target SVG or Canvas yourself. Higher-level libraries tend to have a default — for example Chart.js draws to Canvas, Observable Plot outputs SVG, and Apache ECharts can switch between Canvas and SVG renderers — while GPU-focused libraries such as deck.gl target WebGL and are adding WebGPU. Hybrid setups are common and sensible: a WebGL layer for millions of points, with SVG axes and HTML tooltips on top.
- Default to SVG for typical business charts: it is accessible, stylable and easy to debug.
- Move to Canvas when mark counts reach many thousands or you redraw continuously, and plan for hit testing and pixel density.
- Use WebGL for very large or GPU-friendly workloads, ideally through a library; treat WebGPU as an enhancement for now.
- Reduce and aggregate data before reaching for a heavier renderer.
- Every chart needs a text alternative and access to the underlying data.
From data to decisions#
The best visualization is not the one with the most impressive rendering engine; it is the one that makes the right insight obvious to every reader, loads quickly and stays responsive on the devices your audience actually uses. Start with the question the chart should answer, estimate how much data it really needs, and only then pick the renderer that fits. If you are exploring models in the browser too, our article on practical machine learning with JavaScript pairs naturally with interactive charts.
Building a dashboard, an internal analytics tool or a data-heavy product? Our Custom Web Application Development service covers everything from data pipelines to fast, accessible interactive charts. Tell us about your data and what decisions it needs to support, and we will recommend an approach that fits.
Frequently asked questions
Is SVG or Canvas better for charts?
SVG is usually better for charts with a modest number of marks because every shape is a DOM element that can be styled, made interactive and exposed to assistive technology. Canvas is better when you draw thousands to hundreds of thousands of marks or redraw constantly, at the cost of handling hit testing and accessibility yourself.
When do I actually need WebGL for a chart?
Reach for WebGL when you need to render or animate very large numbers of points, lines or tiles smoothly, such as dense scatter plots, large time series, geospatial layers or 3D scenes. For typical business dashboards, SVG or Canvas is simpler and perfectly fast.
Should I use WebGPU instead of WebGL?
WebGPU offers a more modern API and compute shaders, but MDN still lists it as not yet Baseline because support is not universal across widely used browsers. For production charts today, use WebGL or a library that can fall back to it, and treat WebGPU as a progressive enhancement.
How do I make a Canvas or WebGL chart accessible?
Give the chart a text alternative that states the key insight, provide the underlying data as an HTML table or downloadable file, and make interactive features available by keyboard. Fallback content placed inside the canvas element is exposed to assistive technology, and an adjacent summary helps every reader.
