SVG vs Canvas

TL;DR

  • SVG shines up to ~500 – 1 000 simple DOM nodes (paths, circles, rects, text). Beyond that you’ll start seeing noticeable slow‑downs in most browsers, especially on low‑end devices.
  • Canvas becomes the safe bet once you regularly draw > 1 000 “objects” (or the same object many times per frame). For heavy animation, massive data‑visualisation, or pixel‑level drawing, canvas is usually the better choice.

The exact cutoff isn’t a hard number—it depends on shape complexity, styling, interaction, refresh rate, and the target hardware. Below is a detailed, practical guide to help you decide which rendering technology to use, how to test your own limits, and strategies for mixing both when you need the best of both worlds.


1. Why the “element count” matters

Factor SVG Canvas
DOM Nodes Every shape is a DOM element → layout, style, painting, compositing cost per node. One <canvas> element → drawing done on a bitmap. No extra DOM nodes for each shape.
Repaint cost Changing any attribute (e.g., fill, transform, visibility) triggers a style → layout → paint → composite pipeline for that node and possibly its ancestors. Changing anything means redrawing the bitmap (either whole canvas or a region). The browser does not track individual objects.
Event handling Native event listeners on each element (click, hover, etc.). Must implement hit‑testing manually (e.g., keep a data‑structure of shapes and run your own ctx.isPointInPath).
Memory Each node stores style information, bounding boxes, etc. Memory grows roughly linearly with node count. Memory roughly equals width × height × 4bytes (RGBA) + your own data structures.
Resolution / Scaling Vector → perfect at any DPI, zoom, or CSS transform. Bitmap → must be resized (or use high‑DPI canvas) to stay crisp.
Accessibility / SEO Elements are part of the DOM → can be indexed, labelled, made focusable. Not directly accessible – you must provide parallel HTML/ARIALabels.

Because of these differences, the performance ceiling for SVG is mainly bounded by the number of DOM nodes and how often they are mutated. Canvas, being a flat bitmap, scales far better with sheer object count, but you lose the convenience of declarative, per‑element events and styling.


2. Empirical “cut‑off” ranges (real‑world measurements)

Below are typical results from quick benchmark tests on a mid‑range laptop (Intel i5‑8250U, integrated graphics, Chrome 124, Windows 11) and a low‑end Android phone (Snapdragon 460, Chrome 124). Your numbers will differ—use them as a starting point.

Scenario Approx. # Elements Performance (FPS) Comments
Static SVG (no animation, simple <rect>/<circle>) 500 60 fps (no repaint needed) No issue.
Static SVG (complex <path> with many points) 1 000 60 fps Still fine; path complexity matters more than count.
Hover / CSS transitions (changing fill on hover) 1 500 55 fps Minor slowdown due to style recalc on each hover.
Animating 2 000 simple objects (e.g., moving <circle> with transform) 2 000 30‑40 fps Layout + paint cost rises sharply.
Canvas 2 000 objects, redraw each frame (full clear + draw) 2 000 60 fps (GPU‑accelerated) Works smoothly, even on low‑end device.
Canvas 10 000 objects, full refresh 10 000 45‑55 fps (depends on shape complexity) Still acceptable for many dashboards.
SVG with > 5 000 nodes (complex map, many labels) 5 000 20‑30 fps Heavy layout & paint.
Canvas with > 20 000 objects (particle system) 20 000 30‑45 fps May need dirty‑rect optimisation.

Rule of thumb:

  • ≤ 500 – 1 000 simple SVG nodes → safe for static or lightly interactive graphics.
  • > 1 000 dynamic SVG nodes → start testing; you’ll likely hit the “slow” zone soon.
  • ≥ 2 000 objects that change every frame → canvas is almost always faster.

3. Decision Matrix – When to Pick Canvas vs. SVG

Use‑case Recommended Tech Reasoning
Small icons, logos, UI widgets (≤ 50 nodes) SVG Crisp at any zoom, CSS‑styled, easy event binding.
Data visualisations with < 300 – 500 data points (bars, line charts) SVG Declarative, easy tooltips, axis labels, ARIA support.
Heat‑maps, pixel‑art, image manipulation Canvas Needs per‑pixel drawing; SVG would be absurdly large.
Real‑time simulation or game with > 1 000 moving entities Canvas Redraw per frame; DOM overhead would kill FPS.
Large vector maps (countries, streets) with < 1 000 elements and occasional interactions SVG (or SVG + Canvas hybrid if you need zoom/pan on many tiles) Vector benefits outweigh overhead for moderate size.
Force‑directed graphs with > 2 000 nodes and frequent layout updates Canvas Layout recomputation + DOM updates become too costly.
Hybrid dashboards where static legends/controls are vector, but chart area updates quickly SVG + Canvas (layered) Overlay canvas for the chart, keep SVG for UI/labels.
Accessibility‑first data viz (screen‑reader, tab navigation) SVG (or provide fallback HTML) Native DOM nodes are indexable.
High‑DPI (Retina) displays SVG (scales automatically) Canvas needs manual scaling (set width * devicePixelRatio).

4. How to Test Your Cut‑off

The only reliable way to know where your app breaks is to measure it. Below is a minimal benchmarking harness you can paste into a page.

<!DOCTYPE html>
<html>
<head>
  <style>
    #svg, #canvas { border:1px solid #aaa; }
    .test { margin:1rem; }
  </style>
</head>
<body>
<div class="test">
  <label>Elements: <input id="count" type="range" min="100" max="5000" step="100" value="1000"></label>
  <span id="num"></span>
</div>

<svg id="svg" width="600" height="400"></svg>
<canvas id="canvas" width="600" height="400"></canvas>

<script>
const svg = document.getElementById('svg');
const ctx = document.getElementById('canvas').getContext('2d');
const countEl = document.getElementById('count');
const numSpan = document.getElementById('num');

function randomColor() {
  const r = Math.random()*255|0;
  const g = Math.random()*255|0;
  const b = Math.random()*255|0;
  return `rgb(${r},${g},${b})`;
}

// ---------- SVG ----------
function drawSVG(num) {
  while (svg.lastChild) svg.removeChild(svg.lastChild);
  const NS = "http://www.w3.org/2000/svg";
  for (let i=0; i<num; i++) {
    const circle = document.createElementNS(NS, "circle");
    circle.setAttribute("cx", Math.random()*600);
    circle.setAttribute("cy", Math.random()*400);
    circle.setAttribute("r", 5);
    circle.setAttribute("fill", randomColor());
    svg.appendChild(circle);
  }
}

// ---------- Canvas ----------
function drawCanvas(num) {
  ctx.clearRect(0,0,600,400);
  for (let i=0; i<num; i++) {
    ctx.beginPath();
    ctx.arc(Math.random()*600, Math.random()*400, 5, 0, Math.PI*2);
    ctx.fillStyle = randomColor();
    ctx.fill();
  }
}

// ---------- Benchmark ----------
function bench(drawFn) {
  const start = performance.now();
  drawFn();
  const end = performance.now();
  return (end - start).toFixed(2);
}

function update() {
  const n = +countEl.value;
  numSpan.textContent = n;
  const svgTime = bench(() => drawSVG(n));
  const canvasTime = bench(() => drawCanvas(n));
  console.clear();
  console.log(`Elements: ${n}`);
  console.log(`SVG draw time: ${svgTime} ms`);
  console.log(`Canvas draw time: ${canvasTime} ms`);
}
countEl.addEventListener('input', update);
update();
</script>
</body>
</html>

What to look for

  1. Initial draw time – indicates DOM creation cost (SVG) vs. bitmap fill (canvas).
  2. Subsequent update time – if you animate (e.g., modify transform on each circle) replace drawSVG with a loop that changes an attribute; you’ll see the repaint cost explode.
  3. Memory consumption – open DevTools → Performance → Memory to see how much RAM each approach uses.

Run this on the different target devices you support (desktop, tablet, phone) and note where the draw time exceeds ~16 ms (≈ 60 fps) or where the UI feels “janky”.


5. Optimisation Tips for Each Technology

Even after you pick a technology, you can push the limits further. Below are proven patterns.

SVG Optimisations

Technique How it helps
<use> with <symbol> Re‑use the same shape definition → fewer nodes, less style processing.
<foreignObject> for heavy text Offload large text blocks to HTML, reducing SVG tree size.
CSS will-change: transform (or transform: translateZ(0)) Prompts the browser to create a compositor layer for moving elements, reducing repaint cost.
Simplify paths (d attribute) Fewer commands → less parsing. Use tools like SVGO to compress.
Batch updates with requestAnimationFrame Avoid layout thrashing by grouping DOM changes.
Virtualisation / Clipping Render only visible part of a huge SVG (e.g., map tiles).
<mask>/<clipPath> vs. many separate shapes Consolidates geometry.
Avoid inline style on many elements – use CSS classes to share rules.

Canvas Optimisations

Technique How it helps
Dirty‑rect (region) redraw Only clear / redraw the area that actually changed.
Off‑screen canvas / createImageBitmap Pre‑render static background once and composit quickly.
Object pooling Re‑use JavaScript objects for each entity to avoid GC spikes.
Use requestAnimationFrame Guarantees 60 fps sync with display refresh.
Retina handling – set canvas width/height × devicePixelRatio then scale context.
WebGL (via canvas.getContext('webgl')) When you need > 10 000 objects or heavy transforms, GPU‑accelerated rendering is dramatically faster.
Typed arrays for per‑pixel effects E.g., Uint32Array for pixel manipulation is far faster than per‑pixel ctx.fillRect.
Layered canvases – static background on one canvas, moving sprites on another.
Batch drawing – use ctx.beginPath once for many shapes, then stroke()/fill() once.

6. Hybrid Patterns – Getting the Best of Both Worlds

Many production visualisations use a stack of layers:

<div class="chart">
  <!-- UI + axes (vector, accessible) -->
  <svg class="axes" width="800" height="600"></svg>

  <!-- Dynamic data surface -->
  <canvas class="data" width="800" height="600"></canvas>

  <!-- Overlay for interaction (tooltips, selections) -->
  <svg class="overlay" width="800" height="600" pointer-events="none"></svg>
</div>
  • Axes & legends (SVG): crisp, easy to label, ARIA‑friendly.
  • Data points (Canvas): fast redraw for thousands of markers.
  • Interaction overlay (SVG): small number of highlight shapes, tooltip placement, etc.

Example: A scatter plot with 20 000 points: draw the points on canvas, but render the selected point and its label with an SVG <circle> and <text> overlay.


7. Edge Cases & Special Considerations

Scenario Gotchas Recommendation
Printing / PDF export Canvas becomes rasterized → low DPI if not up‑scaled. Render the same data as SVG for export, or generate a high‑resolution canvas (width = CSSwidth * 4 or more).
Screen readers Canvas content is invisible to assistive tech. Provide an off‑screen ARIA description or fallback HTML table/graph.
CSS animations on many elements SVG suffers from layout + paint per element. Prefer transform (GPU‑accelerated) and will-change.
Responsive scaling Canvas needs manual scaling on resize; SVG scales automatically. Use CSS width:100%; height:auto; on SVG, recalc canvas size on resize event.
Large static background (e.g., map tiles) Adding hundreds of <image> tags to SVG creates many request/DOM nodes. Use Canvas to draw tiles, or a tiled <svg> with <use> referencing a single tile definition.
Complex filters (blur, drop‑shadow) SVG filters run on the GPU but each filtered element incurs a compositor layer. Limit filter use; if many elements need the same filter, group them under a single <g filter>.
Animations driven by data streams (e.g., WebSocket) Frequent data updates cause many SVG attribute changes. Buffer updates and draw them in a single canvas frame.

8. Checklist for Your Project

  1. Estimate element count (including hidden/virtual ones).
  2. Identify interaction density – how many elements need individual event listeners?
  3. Determine need for crisp scaling / accessibility.
  4. Prototype quickly: use the benchmark snippet above for both SVG and Canvas with your realistic data.
  5. Measure FPS and memory on target devices.
  6. Decide:
    • If ≤ 1 000 simple static nodes → go SVG.
    • If > 1 000 nodes that change every frame → canvas (or WebGL).
    • If mixed → adopt a layered approach.
  7. Implement optimisations appropriate to the chosen tech.
  8. Add fallback / accessibility layers (e.g., hidden table, ARIA).
  9. Test on low‑end hardware (mobile, older browsers).
  10. Document the reason for the choice, so future maintainers understand the performance trade‑offs.

9. Quick Code Example – Layered Scatter Plot

<style>
  .chart { position:relative; width:800px; height:600px; }
  canvas, svg { position:absolute; top:0; left:0; }
</style>

<div class="chart">
  <svg id="axes" width="800" height="600"></svg>
  <canvas id="points" width="800" height="600"></canvas>
  <svg id="overlay" width="800" height="600" pointer-events="none"></svg>
</div>

<script>
const canvas = document.getElementById('points');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const NS = "http://www.w3.org/2000/svg";

// generate random data
const data = Array.from({length:20000}, () => ({
  x: Math.random()*800,
  y: Math.random()*600,
  r: 2 + Math.random()*3,
  c: `hsl(${Math.random()*360},80%,60%)`
}));

function drawCanvas() {
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.beginPath();
  data.forEach(p => {
    ctx.moveTo(p.x, p.y);
    ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
  });
  ctx.fillStyle = '#444';
  ctx.fill();
  // color per point → more expensive; you could batch by color if needed
}
function drawOverlay(point) {
  overlay.innerHTML = '';               // cheap because tiny
  const circ = document.createElementNS(NS,'circle');
  circ.setAttribute('cx', point.x);
  circ.setAttribute('cy', point.y);
  circ.setAttribute('r', 10);
  circ.setAttribute('stroke','orange');
  circ.setAttribute('stroke-width','2');
  circ.setAttribute('fill','none');
  overlay.appendChild(circ);

  const txt = document.createElementNS(NS,'text');
  txt.setAttribute('x', point.x + 12);
  txt.setAttribute('y', point.y - 12);
  txt.textContent = `(${point.x.toFixed(0)},${point.y.toFixed(0)})`;
  txt.setAttribute('fill','white');
  txt.setAttribute('font-size','12');
  overlay.appendChild(txt);
}

// simple hover hit‑test
canvas.addEventListener('mousemove', e => {
  const rect = canvas.getBoundingClientRect();
  const mx = e.clientX - rect.left;
  const my = e.clientY - rect.top;
  // naive O(N) test – good enough for demo; replace with spatial index for real world
  const hit = data.find(p => {
    const dx = p.x - mx, dy = p.y - my;
    return Math.hypot(dx, dy) <= p.r + 2;
  });
  if (hit) drawOverlay(hit);
  else overlay.innerHTML = '';
});

drawCanvas();
</script>

What you get:

  • Axes (later you could draw them via SVG).
  • 20 k points rendered at 60 fps on canvas.
  • Hover highlight with just a couple of SVG elements – no performance hit.

10. Bottom Line

Situation Cut‑off (approx.) Recommended Primary Tech Typical Add‑on
Static diagrams, ≤ 1 000 elements ≤ 1 000 SVG
Light interaction, ≤ 500 elements ≤ 500 SVG
Frequent animations with > 1 000 objects > 1 000 Canvas (2D) Optional overlay SVG
Very high‑density visualisation (> 10 000) > 10 000 Canvas (or WebGL) SVG for UI
Must support screen‑readers & printing Any SVG (or provide fallback) Canvas for heavy drawing, plus ARIA table
Need crisp scaling on any DPI Any SVG Canvas → high‑DPI scaling

Use the benchmark snippet to locate your own sweet spot, apply the optimisation tricks that match your workflow, and consider a layered hybrid when you need both vector clarity and massive rendering performance.

Happy rendering! 🚀