← Назад к вопросам

Как отобразить пользователю большое количество данных приходящих с 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 и оптимизируйте узкие места.