Переход с SVG на Canvas для производительности

Большинство компонентов библиотеки Nivo построено поверх SVG. Такой подход обеспечивает:

  • декларативное описание графики;
  • простую стилизацию через CSS;
  • встроенную поддержку анимаций;
  • удобную интеграцию с React;
  • доступность отдельных DOM-элементов.

Однако при работе с большими объёмами данных SVG начинает создавать серьёзную нагрузку на браузер. Основная проблема заключается в том, что каждый элемент графика становится отдельным DOM-узлом.

Например:

  • 10 000 точек на scatter plot;
  • 5 000 столбцов в bar chart;
  • десятки тысяч линий и сегментов;
  • heatmap с огромной матрицей значений.

В SVG каждый элемент требует:

  • расчёта layout;
  • хранения в DOM;
  • обработки событий;
  • перерисовки браузером.

Canvas работает иначе. Вместо множества DOM-элементов используется единая поверхность рисования. Это резко уменьшает нагрузку на браузер и повышает производительность.


Отличия SVG и Canvas в контексте Nivo

SVG

SVG-компоненты в Nivo обычно импортируются так:

import { ResponsiveLine } from '@nivo/line'

Особенности:

  • каждый элемент — DOM-узел;
  • удобно работать с CSS;
  • высокая интерактивность;
  • хорошая читаемость структуры;
  • удобно отлаживать через DevTools.

Недостатки:

  • деградация производительности при больших объёмах данных;
  • рост потребления памяти;
  • снижение FPS при анимациях;
  • задержки при hover и tooltip.

Canvas

Canvas-версии компонентов импортируются отдельно:

import { ResponsiveLineCanvas } from '@nivo/line'

Особенности:

  • единая поверхность рендеринга;
  • высокая скорость отрисовки;
  • меньше памяти;
  • стабильная работа с большими наборами данных.

Недостатки:

  • отсутствие DOM-элементов;
  • сложнее кастомизировать CSS;
  • ограниченная интерактивность;
  • сложнее реализовывать сложные эффекты.

Компоненты Nivo с поддержкой Canvas

Nivo предоставляет Canvas-версии для наиболее тяжёлых визуализаций.

Line

SVG:

ResponsiveLine

Canvas:

ResponsiveLineCanvas

ScatterPlot

SVG:

ResponsiveScatterPlot

Canvas:

ResponsiveScatterPlotCanvas

Bar

SVG:

ResponsiveBar

Canvas:

ResponsiveBarCanvas

HeatMap

SVG:

ResponsiveHeatMap

Canvas:

ResponsiveHeatMapCanvas

Voronoi

SVG:

ResponsiveVoronoi

Canvas:

ResponsiveVoronoiCanvas

Когда переход на Canvas действительно необходим

Canvas не всегда лучше SVG.

SVG остаётся предпочтительным вариантом при:

  • небольших графиках;
  • сложной интерактивности;
  • необходимости CSS-анимаций;
  • высоких требованиях к accessibility;
  • частой работе с DOM.

Canvas оправдан при:

  • десятках тысяч элементов;
  • real-time графиках;
  • потоковых данных;
  • финансовых графиках;
  • телеметрии;
  • аналитических dashboard;
  • heatmap большого размера;
  • scientific visualization.

Пример проблемы производительности SVG

Допустим, отображается график с 50 000 точек.

SVG-компонент:

import { ResponsiveScatterPlot } from '@nivo/scatterplot'

Каждая точка:

<circle />

50 000 элементов:

  • перегружают DOM;
  • создают задержки hover;
  • тормозят масштабирование;
  • вызывают фризы интерфейса.

В Canvas все точки рисуются напрямую:

ctx.fill()

DOM практически не растёт.


Базовый переход с SVG на Canvas

SVG-вариант

import { ResponsiveLine } from '@nivo/line'

<ResponsiveLine
    data={data}
/>

Canvas-вариант

import { ResponsiveLineCanvas } from '@nivo/line'

<ResponsiveLineCanvas
    data={data}
/>

Во многих случаях этого достаточно.


Изменения в API

API Canvas-компонентов максимально похож на SVG-версии.

Поддерживаются:

  • axes;
  • legends;
  • colors;
  • scales;
  • themes;
  • tooltip;
  • mesh;
  • markers.

Пример:

<ResponsiveLineCanvas
    data={data}
    margin={{ top: 50, right: 50, bottom: 50, left: 60 }}
    xScale={{ type: 'point' }}
    yScale={{
        type: 'linear',
        min: 0,
        max: 'auto'
    }}
    axisBottom={{
        tickSize: 5,
        tickPadding: 5
    }}
    colors={{ scheme: 'category10' }}
/>

Ограничения Canvas-компонентов

Canvas-компоненты не полностью повторяют SVG-функциональность.

Часто отсутствуют:

  • сложные transition-анимации;
  • полноценная поддержка motion;
  • CSS-стилизация элементов;
  • custom SVG layers;
  • arbitrary SVG filters;
  • SVG gradients;
  • DOM-based interaction.

Производительность при анимациях

SVG-анимации:

motionConfig="gentle"
animate={true}

На больших графиках вызывают:

  • drop FPS;
  • repaint storm;
  • высокую нагрузку CPU.

Canvas чаще используется без анимаций:

animate={false}

Это особенно важно в real-time системах.


Использование pixelRatio

Canvas может выглядеть размыто на Retina-дисплеях.

Для повышения качества используется:

pixelRatio={2}

Пример:

<ResponsiveLineCanvas
    data={data}
    pixelRatio={window.devicePixelRatio}
/>

Оптимизация hover-событий

В SVG hover работает через DOM.

Canvas не содержит отдельных элементов, поэтому Nivo реализует внутренние алгоритмы поиска объектов.

Для line chart используется:

useMesh={true}

Пример:

<ResponsiveLineCanvas
    data={data}
    useMesh={true}
/>

Mesh создаёт виртуальную интерактивную карту.


Проблемы mesh при огромных данных

useMesh значительно улучшает UX, но создаёт вычислительную нагрузку.

На очень больших графиках:

  • построение mesh становится дорогим;
  • hover начинает лагать;
  • возрастает время initial render.

Иногда выгоднее отключить mesh:

useMesh={false}

Использование точечного поиска

Для повышения производительности можно ограничивать detection radius.

Пример:

<ResponsiveScatterPlotCanvas
    data={data}
    detectionRadius={6}
/>

Меньший радиус уменьшает объём вычислений.


Работа с потоковыми данными

Canvas особенно эффективен в streaming-сценариях.

Пример:

const [data, setData] = useState(initialData)

useEffect(() => {
    const interval = setInterval(() => {
        setData(generateNextFrame())
    }, 100)

    return () => clearInterval(interval)
}, [])

SVG при постоянных обновлениях:

  • быстро деградирует;
  • вызывает garbage collection spikes;
  • увеличивает layout cost.

Canvas обновляется заметно стабильнее.


Уменьшение количества точек

Даже Canvas не спасает от чрезмерного объёма данных.

Распространённая ошибка:

1000000 points

Правильный подход:

  • sampling;
  • aggregation;
  • clustering;
  • decimation;
  • windowing.

Decimation данных

Перед рендерингом можно уменьшать количество точек.

Пример простого sampling:

const reducedData = data.filter((_, index) => index % 10 === 0)

Более продвинутые алгоритмы:

  • Largest Triangle Three Buckets;
  • MinMax sampling;
  • Douglas-Peucker;
  • temporal aggregation.

Windowing для real-time графиков

Отображается только актуальный диапазон:

const visibleData = data.slice(-1000)

Такой подход:

  • стабилизирует FPS;
  • уменьшает память;
  • ускоряет tooltip;
  • снижает нагрузку CPU.

Особенности tooltip в Canvas

Tooltip остаётся React-компонентом:

tooltip={({ point }) => (
    <div>
        {point.data.x}: {point.data.y}
    </div>
)}

Но поиск активной точки выполняется вручную внутри Canvas-логики.


Кастомный рендеринг Canvas

Некоторые Canvas-компоненты позволяют использовать custom layers.

Пример:

layers={[
    'grid',
    'axes',
    'lines',
    CustomLayer
]}

Canvas-layer получает context:

const CustomLayer = ({ ctx, innerWidth, innerHeight }) => {
    ctx.fillStyle = 'red'

    ctx.fillRect(0, 0, innerWidth, innerHeight)
}

Работа с Canvas Context

Canvas API основан на imperative rendering.

Основные методы:

ctx.beginPath()
ctx.moveTo()
ctx.lineTo()
ctx.stroke()

Для окружностей:

ctx.arc(x, y, radius, 0, Math.PI * 2)

Для текста:

ctx.fillText('Label', x, y)

Управление перерисовкой

Canvas полностью перерисовывается при обновлении.

Поэтому критично:

  • минимизировать state updates;
  • использовать memoization;
  • избегать лишних render cycles.

React.memo

Пример:

const Chart = React.memo(({ data }) => {
    return (
        <ResponsiveLineCanvas data={data} />
    )
})

useMemo для подготовки данных

const preparedData = useMemo(() => {
    return processData(rawData)
}, [rawData])

Без memoization preprocessing может стать bottleneck.


useCallback для handlers

const handleClick = useCallback(point => {
    console.log(point)
}, [])

Offscreen rendering

В тяжёлых приложениях иногда используют:

  • OffscreenCanvas;
  • Web Workers;
  • background rendering.

Nivo напрямую это не предоставляет, но архитектура Canvas позволяет внедрять подобные оптимизации.


Сравнение памяти SVG и Canvas

SVG

Память расходуется на:

  • DOM nodes;
  • style calculation;
  • layout tree;
  • event listeners.

Canvas

Память расходуется в основном на:

  • bitmap buffer;
  • drawing state;
  • internal caches.

При огромных datasets Canvas обычно потребляет значительно меньше памяти.


Измерение производительности

Основные инструменты:

  • Chrome Performance;
  • React Profiler;
  • Memory tab;
  • FPS meter.

Важно анализировать:

  • scripting time;
  • rendering time;
  • paint time;
  • layout cost;
  • memory allocation.

Типичные bottleneck при SVG

Большое количество DOM-узлов

<circle />
<circle />
<circle />

Сложные transition

animate={true}

Частые React re-render

setState(...)

События на тысячах элементов

onMouseMove

Типичные bottleneck при Canvas

Огромный bitmap

Canvas высокого разрешения:

pixelRatio={4}

может резко увеличить потребление памяти.


Сложный custom drawing

Чрезмерное количество:

ctx.save()
ctx.restore()

может замедлять рендер.


Частая полная перерисовка

Canvas не обновляет отдельные элементы — перерисовывается всё.


Практическое сравнение SVG и Canvas

SVG

Подходит для:

  • admin panels;
  • BI dashboards;
  • презентационных графиков;
  • сложных tooltip;
  • accessibility.

Canvas

Подходит для:

  • trading systems;
  • telemetry;
  • monitoring;
  • heatmaps;
  • geospatial visualization;
  • scientific charts;
  • live analytics.

Гибридный подход

Во многих приложениях используют комбинацию:

  • Canvas для основного рендера;
  • SVG для overlay;
  • HTML для tooltip.

Например:

  • линии рисуются через Canvas;
  • выделение точки — через SVG;
  • tooltip — через React Portal.

Влияние Canvas на accessibility

SVG предоставляет:

  • ARIA;
  • focus;
  • screen reader support;
  • keyboard navigation.

Canvas этого не имеет по умолчанию.

При необходимости accessibility добавляют отдельно:

aria-label="Chart"
role="img"

Но полноценная семантика всё равно ограничена.


Retina и масштабирование

На Retina-дисплеях важно учитывать:

pixelRatio={window.devicePixelRatio}

Однако слишком высокие значения:

  • увеличивают bitmap;
  • повышают memory usage;
  • замедляют repaint.

Обычно достаточно:

pixelRatio={2}

SSR и Canvas

SVG легче использовать с server-side rendering.

Canvas зависит от браузерного API:

window
document
canvas

В SSR-фреймворках иногда требуется dynamic import:

const Chart = dynamic(
    () => import('./Chart'),
    { ssr: false }
)

Canvas и экспорт изображений

Canvas удобно экспортируется:

canvas.toDataURL()

или:

canvas.toBlob()

Это полезно для:

  • PDF generation;
  • reports;
  • snapshots;
  • exporting dashboards.

Переход больших проектов на Canvas

Типичная стратегия миграции:

  1. Определение bottleneck-графиков.
  2. Замер FPS и memory.
  3. Замена SVG-компонентов на Canvas.
  4. Отключение тяжёлых анимаций.
  5. Оптимизация datasets.
  6. Настройка memoization.
  7. Профилирование после миграции.

Пример полной миграции

До

import { ResponsiveScatterPlot } from '@nivo/scatterplot'

<ResponsiveScatterPlot
    data={data}
    animate={true}
    useMesh={true}
/>

После

import { ResponsiveScatterPlotCanvas } from '@nivo/scatterplot'

<ResponsiveScatterPlotCanvas
    data={data}
    animate={false}
    useMesh={false}
    pixelRatio={2}
/>

Эффекты миграции на практике

После перехода обычно наблюдаются:

  • уменьшение memory usage;
  • рост FPS;
  • снижение CPU load;
  • ускорение initial render;
  • уменьшение лагов hover;
  • стабильность при streaming updates.

Особенно заметен эффект на графиках с десятками тысяч объектов.


Архитектурные особенности Canvas в Nivo

Canvas-компоненты Nivo:

  • используют imperative drawing;
  • минимизируют React DOM updates;
  • делегируют rendering в Canvas API;
  • сохраняют декларативный React API;
  • поддерживают responsive layout.

Это позволяет сохранять удобство React-разработки при значительно более высокой производительности визуализации.