Performance, rendering cost and canvas trade-offs

Understand what makes an SVG expensive, find the hot spots, and use a decision framework for choosing between SVG, canvas and CSS.

What actually costs time

FactorCostMitigation
DOM node countHigh: every element is a node with style and layoutGroup and merge; simplify paths
FiltersVery high: offscreen buffers per elementPre-render, or apply to a static parent
Masks and clippingHigh: a composite per frameAvoid on animated elements
Path complexityModerate: one fill per subpath is cheap, many tiny ones is notReduce points; simplify in the editor
Strokes on complex pathsModerate to high: stroking is more expensive than fillingPrefer filled shapes
Transitions on geometryVery high: d, width, x trigger layoutAnimate transform and opacity
A large viewBox with fine detailHigh rasterisation cost when scaledMatch the detail to the rendered size
Repeated <use> of a complex symbolCheap: the geometry is sharedUse sprites rather than duplicated markup
// Counting the real cost: an SVG with 12,000 elements is a layout problem,
// not a rendering problem, and it will make the whole page sluggish.
const svg = document.querySelector('svg');
const count = svg.querySelectorAll('*').length;
console.log('elements:', count);

// A budget that keeps things comfortable:
//   under 1,000 elements  - fine anywhere, including animation
//   under 5,000           - fine static, careful with interaction
//   over 10,000           - move to canvas
//   over 50,000           - canvas with tiling, or a raster tile server

// Measuring a render: the Performance panel's "Recalculate Style" and
// "Layout" entries are where SVG shows up. Long tasks with many small
// style recalculations are the signature of too many nodes.
<!-- Animated geometry: this forces layout on every frame. -->
<rect x="0" y="0" width="10" height="10">
  <animate attributeName="width" from="10" to="100" dur="1s" fill="freeze"/>
</rect>

<!-- Animated transform: composited, no layout. -->
<rect x="0" y="0" width="10" height="10" transform="scale(1 1)">
  <animateTransform attributeName="transform" type="scale" from="1 1" to="10 1" dur="1s" fill="freeze"/>
</rect>

<!-- The transform version also scales the corner radius and the stroke, which
     is often what you actually want for a bar growing from its left edge. -->
💡
The single most effective SVG performance rule: animate only transform and opacity. Everything else — d, width, height, x, y, cx — forces the browser to recompute geometry and re-layout the SVG tree on every frame.

Finding the hot spot

// A quick instrumented measurement around a suspect operation.
function measure(label, fn) {
  const start = performance.now();
  const result = fn();
  const elapsed = performance.now() - start;
  console.log(label, elapsed.toFixed(2) + 'ms');
  return result;
}

// Measure the three separate phases: building the markup, inserting it, and
// the first paint that follows.
const html = measure('build markup', () => buildSvgString(data));
measure('insert into DOM', () => { container.innerHTML = html; });
requestAnimationFrame(() => console.log('painted'));

// Reading geometry forces layout. Do it once, outside the loop.
measure('one getBBox', () => svg.querySelector('path').getBBox());

// Comparing a filter on and off is the fastest way to prove it is the culprit.
svg.classList.toggle('with-filter');
measure('toggle filter', () => svg.getBoundingClientRect());
  • Use the Performance panel and look for Recalculate Style, Layout and Paint. A long Paint entry points at a filter, a mask or a large fill area; a long Layout points at too many nodes or animated geometry.
  • Toggle features one at a time. Removing a filter and re-measuring is definitive; reasoning about which one is expensive usually is not.
  • Test on the slowest device you support. A 12,000-node SVG is instant on a desktop and unusable on a five-year-old phone.
  • Count elements rather than bytes. A small file can still be a layout problem if it contains thousands of tiny shapes.

SVG, canvas or CSS

RequirementSVGCanvasCSS
Crisp at any zoomYesNo, unless re-renderedVector shapes yes, effects no
Accessible per-elementYes, with roles and titlesNo, one pixel bufferLimited
Styleable with the page's CSSYes, as a live DOMNoYes
A few hundred interactive marksComfortableWorkable, hit testing by handNo
Tens of thousands of marksNoYesNo
Photo-realistic effectsNoYesLimited
Complex text layoutFragileManualNative
Print and exportVector outputRaster outputVector where applicable
Animated transformsFineManual redrawBest: composited
Inspectable in dev toolsYes, every nodeNoPartially
// A decision function you can actually apply.
function chooseTechnology({ markCount, needsInteraction, needsAccessibility, needsExport, animates }) {
  if (markCount > 10_000) return 'canvas with tiling or server-side tiles';
  if (markCount > 2_000 && !needsAccessibility) return 'canvas';
  if (needsAccessibility && markCount < 2_000) return 'svg';
  if (markCount < 50 && animates) return 'css or svg';
  if (needsExport === 'vector') return 'svg';
  if (needsExport === 'raster') return 'canvas';
  return markCount < 500 ? 'svg' : 'canvas';
}

console.log(chooseTechnology({
  markCount: 240, needsInteraction: true, needsAccessibility: true,
  needsExport: 'vector', animates: true
}));   // -> 'svg'
<!-- The hybrid that covers most dashboards: SVG for the chrome, canvas for the data. -->
<div class="chart" style="position: relative">
  <!-- canvas draws the 20,000 data points -->
  <canvas id="series" style="position: absolute; inset: 0; width: 100%; height: 100%"></canvas>

  <!-- SVG draws the axes, the labels and the annotations, so they stay crisp,
       selectable and accessible -->
  <svg viewBox="0 0 600 320" style="position: relative; pointer-events: none"
       role="img" aria-label="Revenue over time, 20,000 samples">
    <line x1="56" y1="280" x2="584" y2="280" stroke="#cbd5e1"/>
    <text x="56" y="300" font-size="11" fill="#64748b">Jan</text>
    <text x="584" y="300" font-size="11" fill="#64748b" text-anchor="end">Dec</text>
    <line x1="56" y1="180" x2="584" y2="180" stroke="#ef4444" stroke-dasharray="6 4"/>
    <text x="584" y="174" font-size="11" fill="#ef4444" text-anchor="end">Target</text>
  </svg>
</div>

The summary worth remembering: SVG wins on interactivity, accessibility, CSS styling, vector export and inspectability. Canvas wins on volume and on photographic effects. CSS wins on composited animation of a small number of elements. Most real interfaces use two of the three, and the boundary is almost always the number of data marks.

FAQ

How many SVG elements is too many?
There is no hard number because the cost depends on the device, but the useful thresholds are around 1,000 for freely animated content, 5,000 for static graphics with some interaction, and 10,000 as the point where moving to canvas is usually the right call.
What is the cheapest SVG animation?
A transform or an opacity change with a CSS transition or a CSS animation, because the browser can composite it on the GPU without recomputing geometry. Animating d, width or coordinates forces layout every frame.

Filters and visual effects Responsive, fluid and data-driven SVG

Last refreshed 2026-09-18.