SVG и Canvas решают разные задачи визуализации, и их поведение при работе с большими объёмами данных принципиально различается.
SVG представляет каждый графический элемент как DOM-узел. Это обеспечивает:
Однако при увеличении количества элементов SVG резко теряет производительность. Несколько тысяч DOM-узлов уже создают нагрузку на layout, repaint и memory usage.
Canvas работает иначе: вся сцена отрисовывается в одном bitmap-контексте. Это даёт:
Но Canvas не хранит структуру объектов. Отсутствуют встроенные события на элементы, и любая интерактивность требует дополнительного слоя логики.
Гибридная архитектура SVG + Canvas возникает там, где одновременно требуются:
Типичные сценарии:
Ключевая идея: Canvas отвечает за массовый рендеринг, SVG — за интерактивные сущности и UI-слой.
Базовая структура обычно состоит из двух перекрывающихся слоёв:
<canvas> (рендеринг данных);<svg> (оси, тултипы, интерактивные
элементы).Слои синхронизируются через общие шкалы координат D3.
<div class="chart">
<canvas id="canvas-layer"></canvas>
<svg id="svg-layer"></svg>
</div>
Ключевой принцип: Canvas не знает про DOM, SVG не хранит данные — оба зависят от общей модели данных и шкал.
D3 используется как единый слой трансформации данных в экранные координаты:
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, 100])
.range([height, 0]);
Canvas использует те же шкалы для отрисовки:
function renderCanvas(ctx, data) {
ctx.clearRect(0, 0, width, height);
data.forEach(d => {
ctx.beginPath();
ctx.arc(xScale(d.x), yScale(d.y), 2, 0, Math.PI * 2);
ctx.fill();
});
}
SVG слой при этом использует идентичные координаты для интерактивных элементов:
svg.selectAll("circle")
.data(focusPoints)
.join("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 5);
Canvas слой:
SVG слой:
При масштабировании и панорамировании важно синхронизировать оба слоя.
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", (event) => {
const transform = event.transform;
xScale.range([0, width].map(d => transform.applyX(d)));
yScale.range([height, 0].map(d => transform.applyY(d)));
renderCanvas(ctx, data);
renderSvgOverlay();
});
Canvas полностью перерисовывается, SVG обновляет только интерактивные элементы.
Canvas не поддерживает native hit-testing, поэтому требуется отдельный механизм определения попадания курсора в объект.
Подходит только для небольших наборов данных.
function findNearestPoint(mouseX, mouseY) {
return data.find(d => {
const dx = xScale(d.x) - mouseX;
const dy = yScale(d.y) - mouseY;
return Math.sqrt(dx*dx + dy*dy) < 5;
});
}
const tree = d3.quadtree()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.addAll(data);
function getPoint(mx, my) {
return tree.find(mx, my, 5);
}
Quadtree значительно ускоряет поиск ближайшего элемента при больших объёмах данных.
Canvas не генерирует событий по элементам, поэтому события обрабатываются на уровне холста:
canvas.addEventListener("mousemove", (event) => {
const rect = canvas.getBoundingClientRect();
const mx = event.clientX - rect.left;
const my = event.clientY - rect.top;
const point = tree.find(mx, my, 6);
if (point) {
showTooltip(point);
highlightSvg(point);
} else {
hideTooltip();
}
});
SVG слой используется только для визуального отображения состояния hover/selection.
Эффективность гибридного подхода зависит от качества рендеринга Canvas слоя.
Перерисовка выполняется только при изменении данных или трансформации.
Все операции рисования выполняются в одном цикле:
ctx.beginPath();
for (const d of data) {
ctx.moveTo(xScale(d.x), yScale(d.y));
ctx.arc(xScale(d.x), yScale(d.y), 2, 0, Math.PI * 2);
}
ctx.fill();
Использование промежуточного canvas для статичных слоёв (например, фоновых сеток).
Архитектура:
Поток обновления:
Canvas используется для отрисовки линии:
ctx.beginPath();
ctx.moveTo(xScale(data[0].x), yScale(data[0].y));
for (let i = 1; i < data.length; i++) {
ctx.lineTo(xScale(data[i].x), yScale(data[i].y));
}
ctx.stroke();
SVG слой добавляет:
Основные проблемы возникают при:
Для устранения используется единый источник состояния:
При высокой частоте событий mousemove применяется throttling через requestAnimationFrame:
let latestEvent;
canvas.addEventListener("mousemove", (e) => {
latestEvent = e;
});
function tick() {
if (latestEvent) {
handleMouse(latestEvent);
latestEvent = null;
}
requestAnimationFrame(tick);
}
tick();
Это предотвращает избыточные вычисления при движении курсора.
В сложных системах добавляются дополнительные слои:
Гибридизация расширяется до многоуровневой архитектуры, где каждый слой отвечает за строго определённый класс задач: от пиксельного рендеринга до пользовательского взаимодействия и аналитической логики.