Библиотека Nivo активно использует возможности React: декларативный рендеринг, повторное вычисление пропсов, контекст, SVG-деревья и анимации через react-spring. При работе с большими наборами данных производительность графиков начинает зависеть не только от самого браузера, но и от количества React-рендеров, глубины дерева компонентов и объёма вычислений внутри кастомных слоёв.
Профилирование позволяет определить:
Инструмент Profiler входит в состав расширения React DevTools.
Основные возможности:
Для приложений с Nivo Profiler особенно полезен при анализе:
<ResponsiveLine /><ResponsiveBar /><ResponsivePie /><ResponsiveScatterPlot />Для браузеров Chrome и Firefox используется официальное расширение:
После установки во вкладках DevTools появляются:
Типичный пример графика:
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}
/>
)
}
Перед профилированием желательно:
Последовательность действий:
открыть вкладку Profiler;
нажать Record;
выполнить действие:
остановить запись.
После завершения появится timeline рендеров.
Показывает общее время commit-фазы.
Высокие значения означают:
Для SVG-графиков Nivo commit в 20–40 ms уже может вызывать заметные лаги.
Отражает время рендера конкретного компонента.
Особенно важно отслеживать:
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} />
Проблемный код:
<ResponsiveLine
onCl ick={(point) => {
console.log(point)
}}
/>
Каждый рендер создаёт новую функцию.
Оптимизация:
const handleClick = useCallback((point) => {
console.log(point)
}, [])
<ResponsiveLine onCl ick={handleClick} />
Nivo активно использует react-spring.
Анимации особенно дороги при:
Во Flamegraph постоянно появляются новые commit.
Компоненты рендерятся даже без изменения данных.
Отключение:
<ResponsiveLine
animate={false}
/>
Если производительность резко выросла — проблема в animation pipeline.
SVG плохо масштабируется при тысячах элементов.
Например:
enablePoints={true}
Для 10 000 точек создаётся:
<circle>;Profiler покажет огромный commit duration.
Полезно проверить DOM:
document.querySelectorAll('circle').length
При тысячах элементов необходимо:
Многие компоненты имеют Canvas-версии:
Пример:
import { ResponsiveLineCanvas } from '@nivo/line'
<ResponsiveLineCanvas data={data} />
Canvas:
Profiler обычно показывает кратное уменьшение времени рендера.
Tooltip часто становится неожиданным bottleneck.
Проблемный пример:
tooltip={({ point }) => {
const result = expensiveCalculation(point)
return <div>{result}</div>
}}
Tooltip вызывается при каждом hover.
const cache = useMemo(() => new Map(), [])
const CustomTooltip = memo(({ point }) => {
return <div>{point.data.yFormatted}</div>
})
Плохо:
tooltip={() => (
<ComplexDashboard />
)}
Tooltip должен быть максимально лёгким.
Axes в Nivo могут быть дорогими из-за:
axisBottom={{
format: value => expensiveFormat(value)
}}
Formatter вызывается много раз.
const format = useCallback((value) => {
return fastFormat(value)
}, [])
Custom layers — один из самых частых источников проблем.
Пример:
layers={[
'grid',
'markers',
CustomLayer
]}
function CustomLayer(props) {
const computed = hugeDataset.map(...)
}
Layer выполняется при каждом рендере.
const computed = useMemo(() => {
return hugeDataset.map(...)
}, [hugeDataset])
Большинство пользовательских компонентов вокруг Nivo должны мемоизироваться.
Пример:
const Chart = memo(function Chart({ data }) {
return (
<ResponsiveLine data={data} />
)
})
Profiler показывает:
Если memo не помогает:
Огромная проблема:
const data = rawData.map(...)
На каждом рендере создаётся новый массив.
const data = useMemo(() => {
return rawData.map(...)
}, [rawData])
Realtime charts создают постоянную нагрузку.
Проблемы:
setInterval(update, 100)
10 FPS значительно легче для React, чем 60 FPS.
Иногда проблема не в Nivo.
Пример:
<App>
<Sidebar />
<Dashboard />
<Chart />
</App>
Если App обновляется — Chart тоже рендерится.
const MemoChart = memo(Chart)
Плохо:
<App>
const [hovered, setHovered] = useState()
</App>
Каждый hover обновляет всё приложение.
<ChartContainer>
<Chart />
</ChartContainer>
Состояние должно находиться максимально близко к графику.
Полезная библиотека:
Она показывает:
Особенно полезна для Nivo-конфигураций.
Stacked Bar и Area charts тяжелее обычных.
Причины:
Profiler часто показывает рост render duration в 2–4 раза.
Режим Ranked показывает самые дорогие компоненты.
Типичный список:
1. Points
2. CartesianAxis
3. Line
4. CustomTooltip
Начинать оптимизацию следует сверху списка.
Hover — частая причина лагов.
Особенно в:
При движении мыши:
useMesh={false}
enablePoints={false}
Минимальный JSX:
tooltip={({ point }) => (
<div>{point.data.y}</div>
)}
Большие datasets вызывают:
Особенно опасно:
data.map(item => ({
...item
}))
на каждом рендере.
Лучший результат достигается совместным использованием:
React показывает ререндеры, Chrome — CPU и layout.
Записать baseline profile.
Найти дорогой компонент.
Проверить причины ререндера.
Стабилизировать:
Отключить анимации.
Проверить Canvas-рендерер.
Повторно записать 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 показывает:
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}
/>
После оптимизации обычно снижаются:
И повышаются:
margin={{}}
theme={{}}
axisBottom={{}}
onCl ick={() => {}}
tooltip={() => {}}
enablePoints={true}
<ComplexTooltip />
animate={true}
data.map(...)
filter(...)
sort(...)
Responsive-компоненты отслеживают resize контейнера.
Проблема:
Фиксированные размеры:
<div style={{ width: 800, height: 400 }}>
<ResponsiveLine />
</div>
В React 18 возможны дополнительные ререндеры в development mode.
Profiler может показывать больше render-pass, чем в production.
Анализ производительности всегда следует проводить в production build.
Создание production-сборки:
npm run build
Запуск:
serve -s build
Только production build показывает реальную производительность Nivo.