Компоненты библиотеки Nivo активно реагируют на изменения входных
данных. При каждом обновлении props выполняются:
Если данные обновляются слишком часто, интерфейс начинает:
Особенно заметна проблема в системах:
Для ограничения количества обновлений применяется дебаунсинг.
Дебаунсинг — механизм задержки выполнения функции до тех пор, пока серия вызовов не прекратится.
Принцип работы:
Таким образом большое количество быстрых обновлений превращается в одно.
Выполняет действие после завершения серии событий.
Подходит для:
Ограничивает частоту выполнения.
Подходит для:
Типичный пример:
<ResponsiveLine data={data} />
Если data обновляется десятки раз в секунду:
setInterval(() => {
setData(generateData())
}, 50)
React начинает постоянно перерисовывать компонент.
Внутри Nivo происходят:
При больших наборах данных производительность резко падает.
function debounce(fn, delay) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => {
fn(...args)
}, delay)
}
}
import { ResponsiveLine } from '@nivo/line'
import { useState, useMemo } from 'react'
function Chart() {
const [data, setData] = useState([])
const debouncedUpdate = useMemo(() => {
return debounce((nextData) => {
setData(nextData)
}, 300)
}, [])
function handleIncomingData(nextData) {
debouncedUpdate(nextData)
}
return (
<ResponsiveLine data={data} />
)
}
В production-проектах чаще применяется
lodash.debounce.
Установка:
npm install lodash.debounce
import debounce from 'lodash.debounce'
import { ResponsiveBar } from '@nivo/bar'
import { useMemo, useState } from 'react'
function Dashboard() {
const [data, setData] = useState([])
const updateChart = useMemo(() => {
return debounce((nextData) => {
setData(nextData)
}, 500)
}, [])
function receiveData(nextData) {
updateChart(nextData)
}
return (
<ResponsiveBar data={data} />
)
}
Ошибка:
const updateChart = debounce((data) => {
setData(data)
}, 300)
При каждом рендере создаётся новая debounce-функция.
Последствия:
Правильный подход:
const updateChart = useMemo(() => {
return debounce((data) => {
setData(data)
}, 300)
}, [])
lodash.debounce предоставляет метод
cancel.
Без очистки возможны:
import { useEffect, useMemo } from 'react'
import debounce from 'lodash.debounce'
function Chart() {
const debouncedUpdate = useMemo(() => {
return debounce((data) => {
setData(data)
}, 300)
}, [])
useEffect(() => {
return () => {
debouncedUpdate.cancel()
}
}, [debouncedUpdate])
}
Потоковые системы часто получают обновления десятки раз в секунду.
Пример:
socket.onmess age = (event) => {
const payload = JSON.parse(event.data)
updateChart(payload)
}
Если не использовать debounce:
Эффективный подход:
import { useRef, useEffect, useState } from 'react'
import debounce from 'lodash.debounce'
function RealtimeChart() {
const [data, setData] = useState([])
const bufferRef = useRef([])
const flushBuffer = debounce(() => {
setData([...bufferRef.current])
}, 500)
useEffect(() => {
const socket = new WebSocket('ws://localhost:3000')
socket.onmess age = (event) => {
const item = JSON.parse(event.data)
bufferRef.current.push(item)
flushBuffer()
}
return () => {
socket.close()
flushBuffer.cancel()
}
}, [])
return (
<ResponsiveLine data={data} />
)
}
Пользовательские фильтры тоже создают нагрузку.
Пример без debounce:
onCha nge={(e) => {
setFilter(e.target.value)
updateChart()
}}
Каждый символ запускает перерасчёт.
const applyFilter = useMemo(() => {
return debounce((value) => {
const filtered = filterData(dataset, value)
setData(filtered)
}, 300)
}, [])
Nivo использует react-spring для анимаций.
При слишком частых обновлениях:
Debounce стабилизирует поток изменений.
Иногда полезно отключать анимацию при потоковых данных.
<ResponsiveLine
data={data}
animate={false}
/>
Особенно эффективно при:
React 18 позволяет снижать приоритет тяжёлых обновлений.
import { useTransition } from 'react'
const [isPending, startTransition] = useTransition()
const updateChart = useMemo(() => {
return debounce((nextData) => {
startTransition(() => {
setData(nextData)
})
}, 300)
}, [])
Преимущества:
Nivo реагирует на размеры контейнера.
Во время resize браузер генерирует множество событий.
window.addEventListener('resize', () => {
setWidth(window.innerWidth)
})
Это вызывает постоянные рендеры графиков.
useEffect(() => {
const handleResize = debounce(() => {
setWidth(window.innerWidth)
}, 200)
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
handleResize.cancel()
}
}, [])
Иногда основная нагрузка связана не с самим Nivo, а с подготовкой данных.
Например:
const transformed = expensiveTransform(rawData)
Вычисление запускается при каждом обновлении.
const recalculate = useMemo(() => {
return debounce((dataset) => {
const result = expensiveTransform(dataset)
setPreparedData(result)
}, 400)
}, [])
Некоторые графики запрашивают данные динамически:
fetch('/api/chart?filter=' + value)
Без debounce:
const loadData = useMemo(() => {
return debounce(async (filter) => {
const response = await fetch(`/api/chart?filter=${filter}`)
const json = await response.json()
setData(json)
}, 500)
}, [])
| Сценарий | Задержка |
|---|---|
| Ввод текста | 200–300 мс |
| Resize | 100–200 мс |
| Realtime-графики | 300–1000 мс |
| Серверные запросы | 400–800 мс |
| Тяжёлые вычисления | 500–1500 мс |
Слишком агрессивный debounce приводит к:
Особенно критично для:
Иногда задержка меняется динамически.
Пример:
const delay = isHeavyDataset ? 1000 : 200
Для больших наборов данных обновления можно замедлять.
Даже при debounce компонент может ререндериться из-за новых ссылок.
export default React.memo(MyChart)
В сочетании с debounce это значительно уменьшает нагрузку.
Плохой вариант:
<ResponsiveLine data={[...data]} />
Каждый рендер создаёт новый массив.
Лучше:
<ResponsiveLine data={data} />
useRef позволяет обновлять буфер без рендера.
const dataRef = useRef([])
Это особенно полезно для:
Часто применяется следующая схема:
useRef.<ResponsiveLine
data={data}
animate={false}
motionConfig="stiff"
enablePoints={false}
useMesh={false}
isInteractive={false}
/>
Такая конфигурация уменьшает:
При оптимизации графиков полезно измерять:
Инструменты:
const fn = debounce(...)
debounced.cancel()
10 ms
Практически бесполезно.
5000 ms
Интерфейс становится «ленивым».
Создаёт множество таймеров и ухудшает производительность.
animate={false};useRef.