← Назад к вопросам
Как отобразить пользователю большое количество данных приходящих с Backend если они постоянно отправляются через WebSocket?
2.2 Middle🔥 181 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Отображение больших объёмов WebSocket данных в реальном времени
Это сложная задача, потому что нужно балансировать между актуальностью данных, производительностью и UX. Вот несколько стратегий.
1. Батчинг обновлений (группировка изменений)
Проблема: если WebSocket отправляет данные каждые 100мс, обновление UI на каждое сообщение будет очень дорого.
Решение: накапливаем обновления и применяем их порциями.
import { useState, useEffect, useRef } from 'react'
function useWebSocketBatching(url, batchInterval = 500) {
const [data, setData] = useState([])
const batchRef = useRef([]) // Накапливаем обновления
const wsRef = useRef(null)
useEffect(() => {
wsRef.current = new WebSocket(url)
wsRef.current.onmessage = (event) => {
const update = JSON.parse(event.data)
batchRef.current.push(update)
}
// Применяем батч обновлений через интервал
const batchTimer = setInterval(() => {
if (batchRef.current.length > 0) {
setData(prev => {
// Применяем все накопленные обновления
const updated = [...prev]
for (const update of batchRef.current) {
const index = updated.findIndex(item => item.id === update.id)
if (index >= 0) {
updated[index] = { ...updated[index], ...update }
} else {
updated.push(update)
}
}
batchRef.current = []
return updated
})
}
}, batchInterval)
return () => {
clearInterval(batchTimer)
wsRef.current?.close()
}
}, [])
return data
}
2. Виртуализация списка (virtualization)
Если у вас очень большой список, рендеритесь только видимые элементы:
import { useEffect, useRef, useState } from 'react'
function VirtualizedWebSocketList({ url }) {
const [items, setItems] = useState([])
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 50 })
const containerRef = useRef(null)
const itemHeight = 50 // пиксели
useEffect(() => {
const ws = new WebSocket(url)
const batch = []
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
batch.push(update)
if (batch.length >= 100) {
setItems(prev => {
const updated = [...prev]
for (const item of batch) {
const idx = updated.findIndex(i => i.id === item.id)
if (idx >= 0) updated[idx] = item
else updated.push(item)
}
batch.length = 0
return updated
})
}
}
return () => ws.close()
}, [])
const handleScroll = (e) => {
const scrollTop = e.target.scrollTop
const start = Math.floor(scrollTop / itemHeight)
const end = start + Math.ceil(containerRef.current.clientHeight / itemHeight) + 10
setVisibleRange({ start, end })
}
const visibleItems = items.slice(visibleRange.start, visibleRange.end)
const offsetY = visibleRange.start * itemHeight
return (
<div
ref={containerRef}
onScroll={handleScroll}
style={{
height: '600px',
overflow: 'auto',
border: '1px solid #ccc'
}}
>
<div style={{ height: items.length * itemHeight }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map(item => (
<div key={item.id} style={{ height: itemHeight }}>
{item.name}: {item.value}
</div>
))}
</div>
</div>
</div>
)
}
Или используйте готовую библиотеку react-window:
import { FixedSizeList } from 'react-window'
function VirtualListWithWindow({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}: {items[index].value}
</div>
)
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
)
}
3. Дебаунсинг и throttling обновлений
function useWebSocketThrottled(url, throttleMs = 1000) {
const [data, setData] = useState([])
const lastUpdateRef = useRef(0)
const pendingRef = useRef(null)
useEffect(() => {
const ws = new WebSocket(url)
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
pendingRef.current = update
const now = Date.now()
if (now - lastUpdateRef.current >= throttleMs) {
// Применяем обновление сразу если прошло достаточно времени
setData(prev => {
const updated = [...prev]
const idx = updated.findIndex(i => i.id === update.id)
if (idx >= 0) updated[idx] = update
else updated.push(update)
return updated
})
lastUpdateRef.current = now
pendingRef.current = null
}
}
// Периодически применяем отложенные обновления
const timer = setInterval(() => {
if (pendingRef.current) {
setData(prev => {
const updated = [...prev]
const idx = updated.findIndex(i => i.id === pendingRef.current.id)
if (idx >= 0) updated[idx] = pendingRef.current
else updated.push(pendingRef.current)
return updated
})
lastUpdateRef.current = Date.now()
pendingRef.current = null
}
}, throttleMs)
return () => {
clearInterval(timer)
ws.close()
}
}, [])
return data
}
4. Фильтрация и агрегирование на фронте
function useWebSocketFiltered(url, filterFn) {
const [data, setData] = useState([])
const [stats, setStats] = useState({ total: 0, processed: 0 })
useEffect(() => {
const ws = new WebSocket(url)
const batch = []
let received = 0
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
received++
// Фильтруем на фронте
if (filterFn && !filterFn(update)) {
return
}
batch.push(update)
// Применяем батч через интервал
if (batch.length >= 50 || received % 100 === 0) {
setData(prev => {
const updated = [...prev]
for (const item of batch) {
const idx = updated.findIndex(i => i.id === item.id)
if (idx >= 0) updated[idx] = item
else updated.push(item)
}
batch.length = 0
return updated.slice(-1000) // Храним последние 1000
})
setStats({
total: received,
processed: batch.length
})
}
}
return () => ws.close()
}, [])
return { data, stats }
}
5. Использование Web Worker для обработки данных
Для очень больших объёмов обработка происходит в отдельном потоке:
// worker.js
let data = []
self.onmessage = (event) => {
const { type, payload } = event.data
if (type === 'UPDATE_BATCH') {
// Обработка данных в отдельном потоке
for (const item of payload) {
const idx = data.findIndex(i => i.id === item.id)
if (idx >= 0) {
data[idx] = { ...data[idx], ...item }
} else {
data.push(item)
}
}
// Отправляем результат обратно
self.postMessage({
type: 'BATCH_PROCESSED',
payload: data.slice(-1000)
})
}
}
// component.js
function useWebSocketWorker(url) {
const [data, setData] = useState([])
const workerRef = useRef(new Worker('worker.js'))
const batchRef = useRef([])
useEffect(() => {
const ws = new WebSocket(url)
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
batchRef.current.push(update)
if (batchRef.current.length >= 100) {
// Отправляем обработку в worker
workerRef.current.postMessage({
type: 'UPDATE_BATCH',
payload: batchRef.current
})
batchRef.current = []
}
}
workerRef.current.onmessage = (event) => {
if (event.data.type === 'BATCH_PROCESSED') {
setData(event.data.payload)
}
}
return () => ws.close()
}, [])
return data
}
6. Хранилище с очисткой старых данных
class CircularBuffer {
constructor(maxSize = 10000) {
this.maxSize = maxSize
this.data = new Map()
this.order = []
}
set(key, value) {
if (this.data.has(key)) {
const idx = this.order.indexOf(key)
this.order.splice(idx, 1)
}
if (this.data.size >= this.maxSize) {
const oldest = this.order.shift()
this.data.delete(oldest)
}
this.data.set(key, value)
this.order.push(key)
}
getAll() {
return Array.from(this.data.values())
}
}
function useWebSocketCircular(url, maxSize = 10000) {
const [data, setData] = useState([])
const bufferRef = useRef(new CircularBuffer(maxSize))
useEffect(() => {
const ws = new WebSocket(url)
let updateCount = 0
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
bufferRef.current.set(update.id, update)
updateCount++
// Обновляем UI каждые 500мс
if (updateCount % 50 === 0) {
setData([...bufferRef.current.getAll()])
}
}
return () => ws.close()
}, [])
return data
}
7. Визуализация с canvas вместо DOM
Для тепловых карт, графиков, матриц используйте canvas:
function useCanvasVisualization(url) {
const canvasRef = useRef(null)
const dataRef = useRef([])
useEffect(() => {
const ws = new WebSocket(url)
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
dataRef.current.push(update)
// Отрисовка на canvas вместо DOM
ctx.clearRect(0, 0, canvas.width, canvas.height)
dataRef.current.forEach((item, idx) => {
const x = (idx % 100) * 10
const y = Math.floor(idx / 100) * 10
ctx.fillStyle = item.value > 50 ? 'red' : 'blue'
ctx.fillRect(x, y, 10, 10)
})
// Убираем старые данные
if (dataRef.current.length > 100000) {
dataRef.current = dataRef.current.slice(-50000)
}
}
return () => ws.close()
}, [])
return <canvas ref={canvasRef} width={1000} height={1000} />
}
Рекомендации по выбору стратегии
- Батчинг (500мс-1с) - хороший первый шаг для большинства случаев
- Виртуализация - если список очень длинный (1000+ элементов)
- Web Worker - если обработка данных дорогая
- Canvas - если нужна визуализация огромных объёмов (тепловые карты)
- Фильтрация на фронте - сокращает объём отрисовки
- Циклический буфер - ограничивает памяти для бесконечных потоков
Лучшая практика: комбинируйте подходы - батчинг + виртуализация + фильтрация.
Мониторьте производительность в DevTools Performance tab и оптимизируйте узкие места.