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
| Factor | Cost | Mitigation |
|---|---|---|
| DOM node count | High: every element is a node with style and layout | Group and merge; simplify paths |
| Filters | Very high: offscreen buffers per element | Pre-render, or apply to a static parent |
| Masks and clipping | High: a composite per frame | Avoid on animated elements |
| Path complexity | Moderate: one fill per subpath is cheap, many tiny ones is not | Reduce points; simplify in the editor |
| Strokes on complex paths | Moderate to high: stroking is more expensive than filling | Prefer filled shapes |
| Transitions on geometry | Very high: d, width, x trigger layout | Animate transform and opacity |
A large viewBox with fine detail | High rasterisation cost when scaled | Match the detail to the rendered size |
Repeated <use> of a complex symbol | Cheap: the geometry is shared | Use 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
| Requirement | SVG | Canvas | CSS |
|---|---|---|---|
| Crisp at any zoom | Yes | No, unless re-rendered | Vector shapes yes, effects no |
| Accessible per-element | Yes, with roles and titles | No, one pixel buffer | Limited |
| Styleable with the page's CSS | Yes, as a live DOM | No | Yes |
| A few hundred interactive marks | Comfortable | Workable, hit testing by hand | No |
| Tens of thousands of marks | No | Yes | No |
| Photo-realistic effects | No | Yes | Limited |
| Complex text layout | Fragile | Manual | Native |
| Print and export | Vector output | Raster output | Vector where applicable |
| Animated transforms | Fine | Manual redraw | Best: composited |
| Inspectable in dev tools | Yes, every node | No | Partially |
// 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.Related
Filters and visual effects Responsive, fluid and data-driven SVG
Last refreshed 2026-09-18.