How to svg vs. html5 canvas: performance trade-offs and decision guide
- Step 1Count the live elements and how often they change — SVG cost scales with the number of DOM nodes and, critically, how often you create/remove/mutate them. A static 2,000-node infographic is usually fine; 2,000 nodes you reposition every frame is not. Tally both numbers before deciding.
- Step 2Profile your current SVG in DevTools — Open the Performance panel, record an interaction or animation, and look for recurring Layout (reflow) and Paint costs every frame. Heavy per-frame layout/recalc-style is the classic signature that the DOM, not your code, is the bottleneck.
- Step 3Estimate the redraw cost on the Canvas side — Canvas charges you for every draw call every frame — it has no 'only this shape changed' optimisation. Many small shapes redrawn fully each frame can erase the win. Budget your per-frame call count, not just element count.
- Step 4Prototype both at your real scale — Build a minimal version of your scene in SVG and in Canvas at your true element count. Drive both with
requestAnimationFrameand time frames withperformance.now(). Measure; don't trust a blog's numbers (or this page's) for your exact workload. - Step 5Decide, or go hybrid — If Canvas wins decisively, migrate the hot-path. If the win is marginal but you need DOM features (CSS theming, click targets, screen-reader labels), keep SVG. The common middle ground: Canvas for the dense rendering layer, SVG/HTML for labels, axes, and interactive chrome on top.
- Step 6Convert your path shapes to Canvas calls — When you commit to Canvas, run each path-based asset through the Canvas Exporter to get
draw<Name>(ctx)functions instead of retypingctx.bezierCurveTo()by hand. Remember its scope: paths and their fill/stroke colours, no text/gradients, arcs approximated.
Retained-mode (SVG) vs. immediate-mode (Canvas)
The architectural difference that drives every performance trade-off. Neither is 'better' — they cost in different places.
| Dimension | SVG (retained / DOM) | Canvas (immediate / bitmap) |
|---|---|---|
| State model | Browser retains every shape as a DOM node | You hold all state; canvas keeps only pixels |
| Cost scales with | Number of nodes + mutation frequency (layout/paint) | Number of draw calls per frame |
| Styling | Full CSS, classes, media queries, :hover | None — you set fillStyle/lineWidth in code |
| Events / hit-testing | Per-element DOM events for free | Manual: math or ctx.isPointInPath against re-drawn paths |
| Accessibility | Real DOM — title/desc, focusable elements | Opaque bitmap; you supply fallback content yourself |
| Resolution independence | Vectors stay crisp at any zoom/DPR | Bitmap — you must redraw for DPR / on resize |
| Sweet spot | Up to ~1,000–2,000 mostly-static, interactive shapes | Many thousands of shapes, or heavy per-frame redraw |
Rough crossover guidance
Indicative thresholds, not benchmarks — actual crossover depends on shape complexity, mutation rate, and device. Always profile your own scene. Numbers describe where DOM overhead typically starts to dominate.
| Scenario | SVG comfortable | Consider Canvas |
|---|---|---|
| Static illustration / icon set | Up to a few thousand nodes | Rarely needed unless paint area is huge |
| Interactive chart, occasional updates | Hundreds of elements | Above ~1,000 elements or frequent re-render |
| Animated dashboard / data viz | A few hundred animated nodes | Above ~200–500 animated nodes |
| Particle system / game scene | Tens of nodes | Almost always Canvas (or WebGL) |
| Map with dense geometry | Simplified, low-zoom only | Dense polylines → Canvas / WebGL tiles |
Cookbook
Concrete decision walk-throughs — read these as 'if your situation looks like this, here's the call'.
A static infographic — stay on SVG
300 shapes, drawn once, no animation, needs crisp print export and clickable regions. This is SVG's home turf — migrating to Canvas would lose accessibility and CSS for zero performance gain.
Scene: 300 paths/rects, rendered once, hover tooltips, print-ready SVG cost: one layout/paint, then idle. Hover = cheap CSS. Canvas cost: you'd hand-roll hit-testing + redraw on hover. Verdict: keep SVG. No bottleneck exists to fix.
A 5,000-point scatter plot that pans — migrate the points
SVG creates 5,000 <circle> nodes; panning forces layout work proportional to node count every frame. Canvas draws 5,000 arcs into a bitmap with no DOM bookkeeping.
SVG: 5,000 <circle> nodes → janky pan, high layout cost/frame
Canvas: for (p of points) { ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, 2*Math.PI); ctx.fill(); }
→ one bitmap, smooth pan
Verdict: Canvas for the point layer; SVG for axes + labels.The hybrid pattern most apps land on
Dense data on Canvas, interactive/labelled chrome on SVG or HTML above it. You get Canvas throughput plus DOM events, CSS, and accessibility where they matter.
<div class="chart">
<canvas id="plot"></canvas> <!-- 50k points, immediate-mode -->
<svg class="overlay"> <!-- axes, gridlines, legend -->
<g class="axis">…</g>
<text class="label">Revenue</text> <!-- selectable, themeable -->
</svg>
</div>
→ Canvas for the hot path, SVG/DOM for everything interactive.Measuring your own crossover
Don't trust thresholds blindly. Time both implementations at your real element count with the same animation loop.
let frames = 0, t0 = performance.now();
function loop() {
draw(); // your SVG mutation OR canvas redraw
if (++frames === 120) {
const fps = 120000 / (performance.now() - t0);
console.log('fps:', fps.toFixed(1));
return;
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
→ run for SVG build and Canvas build; compare fps at N elements.Migrating a path asset without retyping it
Once you commit to Canvas, the tedious part is converting shapes to draw calls. The exporter does that for <path> geometry — note its limits so you know what you still hand-write.
Drop badge.svg → /svg-tools/svg-canvas-exporter →
export function drawBadge(ctx) { ctx.save(); … ctx.restore(); }
Still manual after export:
• any <text> → outline first, or ctx.fillText yourself
• gradients → ctx.createLinearGradient(...)
• arcs (A) → exported as lineTo; replace with ctx.arc if needed
• stroke-width → set ctx.lineWidth before the callEdge cases and what actually happens
Few elements but constant mutation
SVG can still struggleElement count isn't the only axis. Even a few hundred SVG nodes can stutter if you restyle or reposition them every frame, because each mutation can trigger layout/paint. If a small SVG scene animates badly, Canvas (or batching DOM writes) may still help — profile the per-frame layout cost.
Many elements but fully static
SVG fineA large but static SVG pays its layout/paint cost once and then sits idle. Don't migrate a static illustration to Canvas for 'performance' — there's no per-frame cost to remove, and you'd lose CSS, selectable text, and accessibility for nothing.
Canvas needs crisp rendering on a Retina / high-DPR screen
Manual DPR scalingSVG stays sharp at any device pixel ratio automatically. Canvas is a bitmap — you must size the backing store to width * devicePixelRatio and ctx.scale(dpr, dpr), and redraw on DPR or size changes, or the output looks blurry. This is extra code SVG never required.
You need per-shape click/hover after migrating to Canvas
Hit-testing is on youCanvas has no DOM events per shape. You reimplement hit-testing — bounding-box math, a spatial index, or re-drawing each path and calling ctx.isPointInPath. If your scene is interaction-heavy, the engineering cost of leaving the DOM can outweigh the render win.
Accessibility and SEO matter
Canvas is opaqueSVG is real DOM: <title>/<desc>, ARIA, focusable elements, and machine-readable text. A canvas is a single opaque image to assistive tech and crawlers. If your graphic conveys information that must be accessible, keep it in SVG or provide an equivalent DOM fallback alongside the canvas.
Expecting the Canvas Exporter to convert a whole illustration
Path-onlyThe exporter translates <path> geometry plus literal fill/stroke attributes — not <text>, not <rect>/<circle> primitives, not gradients, and arcs only approximately. A complex illustration won't round-trip cleanly. Flatten primitives to paths, outline text, and flatten gradients first, or hand-write the parts it can't cover.
Tens of thousands of shapes at 60fps
Consider WebGLCanvas 2D beats SVG at high counts but is still CPU-bound. Past tens of thousands of shapes redrawn each frame, even Canvas 2D drops frames. At that scale a GPU path (WebGL / a library like PixiJS or regl) is the next step — Canvas 2D is the middle tier, not the ceiling.
Frequent full-canvas clears dominate the frame
Redraw costImmediate-mode means you typically clearRect and repaint everything each frame. If only a small region changed, full repaints waste work. Dirty-rectangle redrawing or layering multiple canvases (static background + dynamic foreground) recovers the lost time — an optimisation SVG handles for you via partial invalidation.
Frequently asked questions
At what element count does Canvas outperform SVG?
There's no single number, but as a rule of thumb the DOM starts to dominate above roughly 1,000 static elements and 200–500 animated ones. The real driver is mutation frequency, not raw count — a static SVG of several thousand nodes is fine, while a few hundred nodes you change every frame may not be. Profile your own scene.
Is SVG always better for static graphics and Canvas for animation?
Mostly, but not absolutely. SVG shines for static, interactive, accessible graphics regardless of animation. Canvas wins when you're redrawing a lot every frame or pushing thousands of shapes. A static SVG with a simple CSS animation is very efficient; the DOM cost only bites when nodes are created, removed, or mutated frequently.
Can I use SVG and Canvas together on one page?
Yes, and it's the most common production pattern. Put the data-dense layer (points, particles, heatmap) on a Canvas, and overlay axes, labels, legends, and interactive controls as SVG or HTML. You keep Canvas throughput plus DOM events, CSS theming, and accessibility where they matter.
What do I lose by moving from SVG to Canvas?
CSS styling, per-element DOM events, automatic hit-testing, resolution independence (you handle device-pixel-ratio yourself), and built-in accessibility/SEO. You also take on redrawing the scene yourself on every change. Those costs are why many teams keep interactive/labelled parts in SVG even after moving the hot path to Canvas.
Does WebGL make Canvas 2D obsolete?
No. For most 2D work, Canvas 2D is far simpler than WebGL and fast enough. WebGL is the next tier up — for 3D, or for GPU-accelerated 2D with tens of thousands of shapes at 60fps. Think SVG → Canvas 2D → WebGL as escalating performance/complexity steps.
How do I actually measure the crossover for my app?
Build minimal SVG and Canvas versions of your scene at your true element count, drive both with requestAnimationFrame, and time frames with performance.now() to get fps. Also record in DevTools' Performance panel and watch for recurring Layout/Paint on the SVG side — that's the DOM-overhead signature.
Why is my SVG animation janky with only a few hundred elements?
Likely layout thrashing: changing geometry/attributes that force the browser to recompute layout every frame, or interleaving reads and writes to the DOM. Batch writes, animate with transforms/opacity where possible, or move the animated layer to Canvas. Profile to confirm it's layout, not your own JS, that's the cost.
Will the Canvas Exporter migrate my entire SVG automatically?
Only the <path> geometry and each path's literal fill/stroke colour. It doesn't handle <text>, <rect>/<circle> primitives, gradients, filters, or precise arcs. For a full migration you flatten primitives to paths, outline text, and flatten gradients first, then hand-finish anything the exporter can't represent.
Does Canvas use less memory than SVG?
Generally yes at high element counts — Canvas holds one bitmap plus your data, while SVG holds a DOM node (with layout boxes and style data) per shape. But Canvas memory scales with canvas pixel dimensions × DPR, so a huge high-DPR canvas can itself be heavy. Measure both for your dimensions.
Can I keep crisp text if I go to Canvas?
Yes — ctx.fillText() renders text sharply with font settings you control. Note the Canvas Exporter does not convert SVG <text> for you; you either add fillText calls yourself or outline the text to paths first (e.g. with svg-font-to-path) and export those paths.
Is the exporter's output fast at runtime?
Yes — it's plain ctx.* draw calls with no parsing or DOM at runtime, which is exactly the immediate-mode efficiency you're migrating for. The one-time parse cost happens in the tool, not in your render loop. Just remember it draws at the SVG's original coordinates, so wrap calls in ctx.translate/ctx.scale to position them.
If I only need smaller SVG, not Canvas, what should I use?
Stay in SVG and optimise it: svg-pro-minifier to shrink markup, and svg-path-simplifier to cut redundant points (note it only simplifies pure polyline paths and skips paths containing curves). Those reduce file size and DOM weight without abandoning the DOM model.
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.