Профилирование рендера с React DevTools

Библиотека Nivo активно использует возможности React: декларативный рендеринг, повторное вычисление пропсов, контекст, SVG-деревья и анимации через react-spring. При работе с большими наборами данных производительность графиков начинает зависеть не только от самого браузера, но и от количества React-рендеров, глубины дерева компонентов и объёма вычислений внутри кастомных слоёв.

Профилирование позволяет определить:

  • какие компоненты перерисовываются слишком часто;
  • какие пропсы вызывают каскадный ререндер;
  • сколько времени занимает commit-фаза React;
  • как влияют анимации;
  • какие вычисления необходимо мемоизировать;
  • где SVG становится узким местом;
  • когда требуется переход на Canvas-рендерер.

React DevTools Profiler

Инструмент Profiler входит в состав расширения React DevTools.

Основные возможности:

  • запись рендеров React-компонентов;
  • измерение времени commit-фазы;
  • анализ причин повторного рендера;
  • отображение дерева компонентов;
  • визуализация “горячих” компонентов;
  • сравнение нескольких профилей.

Для приложений с Nivo Profiler особенно полезен при анализе:

  • <ResponsiveLine />
  • <ResponsiveBar />
  • <ResponsivePie />
  • <ResponsiveScatterPlot />
  • кастомных tooltip;
  • пользовательских layers;
  • heavy tick formatting;
  • динамических legend-компонентов.

Установка React DevTools

Для браузеров Chrome и Firefox используется официальное расширение:

  • React Developer Tools

После установки во вкладках DevTools появляются:

  • Components
  • Profiler

Подготовка приложения к профилированию

Типичный пример графика:

import { ResponsiveLine } from '@nivo/line'

export function Chart({ data }) {
  return (
    <ResponsiveLine
      data={data}
      margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
      xScale={{ type: 'point' }}
      yScale={{
        type: 'linear',
        min: 'auto',
        max: 'auto'
      }}
      animate={true}
    />
  )
}

Перед профилированием желательно:

  • отключить React StrictMode;
  • использовать production build;
  • отключить лишние console.log;
  • исключить расширения браузера, влияющие на производительность.

Запуск профилирования

Последовательность действий:

  1. открыть вкладку Profiler;

  2. нажать Record;

  3. выполнить действие:

    • обновление данных;
    • hover по графику;
    • zoom;
    • фильтрацию;
  4. остановить запись.

После завершения появится timeline рендеров.


Основные метрики Profiler

Commit duration

Показывает общее время commit-фазы.

Высокие значения означают:

  • слишком много DOM/SVG-узлов;
  • тяжёлые вычисления;
  • каскадные обновления;
  • дорогие анимации.

Для SVG-графиков Nivo commit в 20–40 ms уже может вызывать заметные лаги.


Render duration

Отражает время рендера конкретного компонента.

Особенно важно отслеживать:

  • CartesianAxis;
  • Line;
  • Points;
  • Grid;
  • Legends;
  • Tooltip;
  • custom layers.

Flamegraph

Flamegraph показывает глубину дерева компонентов и стоимость каждого рендера.

Широкие блоки означают дорогой рендер.

Пример проблемной структуры:

ResponsiveLine
 ├── Line
 ├── Points
 │    ├── Point
 │    ├── Point
 │    ├── Point
 │    └── ...
 ├── Axes
 └── Legends

Если компонент Point занимает большую часть времени — проблема в количестве SVG-элементов.


Анализ лишних ререндеров

Нестабильные объекты

Частая проблема:

<ResponsiveLine
  margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
/>

Объект создаётся заново при каждом рендере.

React считает проп новым.

Правильный вариант:

const margin = useMemo(() => ({
  top: 20,
  right: 20,
  bottom: 50,
  left: 60
}), [])

<ResponsiveLine margin={margin} />

Нестабильные callback

Проблемный код:

<ResponsiveLine
  onCl ick={(point) => {
    console.log(point)
  }}
/>

Каждый рендер создаёт новую функцию.

Оптимизация:

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

<ResponsiveLine onCl ick={handleClick} />

Профилирование анимаций

Nivo активно использует react-spring.

Анимации особенно дороги при:

  • большом количестве points;
  • stacked charts;
  • realtime updates;
  • frequent filtering.

Типичный симптом

Во Flamegraph постоянно появляются новые commit.

Компоненты рендерятся даже без изменения данных.


Проверка влияния анимаций

Отключение:

<ResponsiveLine
  animate={false}
/>

Если производительность резко выросла — проблема в animation pipeline.


SVG как источник деградации

SVG плохо масштабируется при тысячах элементов.

Например:

enablePoints={true}

Для 10 000 точек создаётся:

  • 10 000 <circle>;
  • обработчики событий;
  • hover-state;
  • tooltip bindings.

Profiler покажет огромный commit duration.


Анализ количества узлов

Полезно проверить DOM:

document.querySelectorAll('circle').length

При тысячах элементов необходимо:

  • отключать points;
  • уменьшать tick density;
  • переходить на Canvas.

Canvas-рендереры в Nivo

Многие компоненты имеют Canvas-версии:

  • LineCanvas
  • BarCanvas
  • ScatterPlotCanvas

Пример:

import { ResponsiveLineCanvas } from '@nivo/line'

<ResponsiveLineCanvas data={data} />

Canvas:

  • не создаёт тысячи DOM-элементов;
  • быстрее при massive datasets;
  • снижает commit duration.

Profiler обычно показывает кратное уменьшение времени рендера.


Профилирование tooltip

Tooltip часто становится неожиданным bottleneck.

Проблемный пример:

tooltip={({ point }) => {
  const result = expensiveCalculation(point)

  return <div>{result}</div>
}}

Tooltip вызывается при каждом hover.


Оптимизация tooltip

Мемоизация вычислений

const cache = useMemo(() => new Map(), [])

Вынос компонента

const CustomTooltip = memo(({ point }) => {
  return <div>{point.data.yFormatted}</div>
})

Избегание тяжёлого JSX

Плохо:

tooltip={() => (
  <ComplexDashboard />
)}

Tooltip должен быть максимально лёгким.


Профилирование осей

Axes в Nivo могут быть дорогими из-за:

  • formatters;
  • rotate;
  • большого числа ticks;
  • сложных label.

Проблемный formatter

axisBottom={{
  format: value => expensiveFormat(value)
}}

Formatter вызывается много раз.


Оптимизация formatter

const format = useCallback((value) => {
  return fastFormat(value)
}, [])

Профилирование custom layers

Custom layers — один из самых частых источников проблем.

Пример:

layers={[
  'grid',
  'markers',
  CustomLayer
]}

Ошибка: тяжёлые вычисления внутри layer

function CustomLayer(props) {
  const computed = hugeDataset.map(...)
}

Layer выполняется при каждом рендере.


Оптимизация

const computed = useMemo(() => {
  return hugeDataset.map(...)
}, [hugeDataset])

Profiler и React.memo

Большинство пользовательских компонентов вокруг Nivo должны мемоизироваться.

Пример:

const Chart = memo(function Chart({ data }) {
  return (
    <ResponsiveLine data={data} />
  )
})

Проверка эффективности memo

Profiler показывает:

  • компонент серый — ререндер пропущен;
  • компонент цветной — выполнен рендер.

Если memo не помогает:

  • пропсы нестабильны;
  • data создаётся заново;
  • используются inline objects.

Deep equality и data arrays

Огромная проблема:

const data = rawData.map(...)

На каждом рендере создаётся новый массив.


Решение

const data = useMemo(() => {
  return rawData.map(...)
}, [rawData])

Профилирование realtime-графиков

Realtime charts создают постоянную нагрузку.

Проблемы:

  • слишком частые state updates;
  • анимации;
  • полные ререндеры;
  • пересчёт scales.

Ограничение FPS обновлений

setInterval(update, 100)

10 FPS значительно легче для React, чем 60 FPS.


Анализ parent-компонентов

Иногда проблема не в Nivo.

Пример:

<App>
  <Sidebar />
  <Dashboard />
  <Chart />
</App>

Если App обновляется — Chart тоже рендерится.


Изоляция графика

const MemoChart = memo(Chart)

Локализация state

Плохо:

<App>
  const [hovered, setHovered] = useState()
</App>

Каждый hover обновляет всё приложение.


Правильная структура

<ChartContainer>
  <Chart />
</ChartContainer>

Состояние должно находиться максимально близко к графику.


Why Did This Render

Полезная библиотека:

  • why-did-you-render

Она показывает:

  • какие пропсы изменились;
  • почему произошёл ререндер;
  • какие объекты имеют новую ссылку.

Особенно полезна для Nivo-конфигураций.


Профилирование stacked charts

Stacked Bar и Area charts тяжелее обычных.

Причины:

  • больше path;
  • сложные вычисления stacking;
  • больше interpolation;
  • дорогие transitions.

Profiler часто показывает рост render duration в 2–4 раза.


Heatmap Profiler

Режим Ranked показывает самые дорогие компоненты.

Типичный список:

1. Points
2. CartesianAxis
3. Line
4. CustomTooltip

Начинать оптимизацию следует сверху списка.


Профилирование hover-событий

Hover — частая причина лагов.

Особенно в:

  • scatter plot;
  • line chart;
  • swarm plot.

Симптомы

При движении мыши:

  • появляются постоянные commit;
  • FPS падает;
  • tooltip дёргается.

Оптимизация hover

Отключение mesh

useMesh={false}

Отключение points

enablePoints={false}

Упрощение tooltip

Минимальный JSX:

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

Анализ memory pressure

Большие datasets вызывают:

  • GC pauses;
  • memory churn;
  • frequent allocations.

Особенно опасно:

data.map(item => ({
  ...item
}))

на каждом рендере.


Chrome Performance + React Profiler

Лучший результат достигается совместным использованием:

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

React показывает ререндеры, Chrome — CPU и layout.


Типичный workflow оптимизации

Шаг 1

Записать baseline profile.


Шаг 2

Найти дорогой компонент.


Шаг 3

Проверить причины ререндера.


Шаг 4

Стабилизировать:

  • data;
  • callback;
  • config objects.

Шаг 5

Отключить анимации.


Шаг 6

Проверить Canvas-рендерер.


Шаг 7

Повторно записать profile.


Пример комплексной оптимизации

Исходный код

<ResponsiveLine
  data={rawData.map(transform)}
  margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
  animate={true}
  tooltip={({ point }) => (
    <HeavyTooltip point={point} />
  )}
  onCl ick={(point) => handle(point)}
/>

Проблемы

Profiler показывает:

  • постоянные ререндеры;
  • tooltip expensive;
  • animation commits;
  • unstable props.

Оптимизированный вариант

const data = useMemo(() => {
  return rawData.map(transform)
}, [rawData])

const margin = useMemo(() => ({
  top: 20,
  right: 20,
  bottom: 50,
  left: 60
}), [])

const handleClick = useCallback((point) => {
  handle(point)
}, [handle])

const Tooltip = memo(({ point }) => {
  return <div>{point.data.y}</div>
})

<ResponsiveLine
  data={data}
  margin={margin}
  animate={false}
  tooltip={Tooltip}
  onCl ick={handleClick}
/>

Метрики успешной оптимизации

После оптимизации обычно снижаются:

  • commit duration;
  • render duration;
  • число commits;
  • memory allocations.

И повышаются:

  • FPS;
  • responsiveness;
  • smooth hover;
  • скорость фильтрации.

Наиболее частые причины медленного рендера Nivo

Inline objects

margin={{}}
theme={{}}
axisBottom={{}}

Inline callbacks

onCl ick={() => {}}
tooltip={() => {}}

Большие SVG-деревья

enablePoints={true}

Heavy tooltip

<ComplexTooltip />

Анимации

animate={true}

Нестабильные datasets

data.map(...)
filter(...)
sort(...)

Практика анализа flamegraph

Хороший flamegraph

  • мало commit;
  • узкие блоки;
  • редкие обновления;
  • неглубокое дерево.

Плохой flamegraph

  • постоянные commit;
  • широкие блоки;
  • глубокие nested-компоненты;
  • множественные tooltip updates.

Оптимизация responsive-обёрток

Responsive-компоненты отслеживают resize контейнера.

Проблема:

  • frequent resize;
  • flex recalculation;
  • layout thrashing.

Решение

Фиксированные размеры:

<div style={{ width: 800, height: 400 }}>
  <ResponsiveLine />
</div>

Влияние React Concurrent Rendering

В React 18 возможны дополнительные ререндеры в development mode.

Profiler может показывать больше render-pass, чем в production.

Анализ производительности всегда следует проводить в production build.


Проверка production build

Создание production-сборки:

npm run build

Запуск:

serve -s build

Только production build показывает реальную производительность Nivo.