Skip to main content
cd ../blog
ReactWebSocketsPerformance

Streaming live sensor data into React without the jank

By Emeka OkezieJun 18, 20262 min read47 views
Streaming live sensor data into React without the jank

On the gas-monitoring dashboard, sensors across a dozen facilities push readings over WebSockets several times a second. The naive approach (calling setState on every message) melted the UI. Here's the shape that actually held up.

The problem with setState-per-message

Every socket message triggering a render means React reconciles the whole tree hundreds of times a second. Charts stutter, inputs lag, and the tab heats up.

The fix isn't a faster chart library. It's decoupling how often data arrives from how often the UI renders.

Buffer, then flush on a frame

Collect incoming readings in a ref, and flush them to state once per animation frame:

useLiveReadings.ts
import { useEffect, useRef, useState } from "react";
 
export function useLiveReadings(socket: WebSocket) {
  const buffer = useRef<Reading[]>([]);
  const [readings, setReadings] = useState<Reading[]>([]);
 
  useEffect(() => {
    const onMessage = (e: MessageEvent) => {
      buffer.current.push(JSON.parse(e.data));
    };
    socket.addEventListener("message", onMessage);
 
    let frame = requestAnimationFrame(function flush() {
      if (buffer.current.length) {
        setReadings((prev) => merge(prev, buffer.current));
        buffer.current = [];
      }
      frame = requestAnimationFrame(flush);
    });
 
    return () => {
      socket.removeEventListener("message", onMessage);
      cancelAnimationFrame(frame);
    };
  }, [socket]);
 
  return readings;
}

Now the socket can fire as fast as it likes; the UI renders at most once per frame.

Keep the heavy work off the main thread

Parsing and aggregating large batches still blocks paint. Move it to a worker:

const worker = new Worker(new URL("./aggregate.worker.ts", import.meta.url));
worker.postMessage(batch);
worker.onmessage = (e) => setSeries(e.data);

What actually moved the needle

  1. Batch by frame: the single biggest win.
  2. Memoize chart data so unrelated state changes don't recompute series.
  3. Virtualize long tables: only render rows in view.
  4. Throttle non-critical widgets to once a second; not everything needs 60fps.

The result: smooth charts under a firehose of updates, and a main thread free enough to stay interactive.