23

I was building a live ops board that streams a few dozen metrics over a websocket, each shown as a number plus a rolling sparkline. The naive build re-rendered the entire React tree on every message and dropped frames the moment the feed got busy.

What worked was making the model separate the transport from the view: a ring buffer per metric outside React, a fixed-window downsample, and components that read the buffer on an animation frame rather than on every socket message. It also had to handle reconnect and backfill without a flash of empty charts.

How are you deciding the rolling window and downsample factor? I hardcoded 300 points but it feels arbitrary for metrics with very different rates.

THE PROMPT
Build a real-time metrics dashboard (React + TypeScript) fed by a websocket emitting {metric: string, t: number, v: number} messages. Design for load:

Transport vs view separation: keep a per-metric ring buffer (fixed capacity, typed array) OUTSIDE React state. Socket messages write to the buffer only. A single requestAnimationFrame loop snapshots buffers and updates the view at most once per frame - never setState per message.

Downsampling: each sparkline shows a fixed window of {WINDOW} seconds; if raw points exceed the pixel width, downsample with Largest-Triangle-Three-Buckets (LTTB) so spikes survive, not naive every-nth. Recompute only when the window's data changed.

Resilience: on disconnect, show a 'reconnecting' state and keep the last good chart dimmed (no empty flash); on reconnect, backfill from the buffer and resume. Detect a stalled feed (no message in 3x the expected interval) and flag that tile.

Each tile: current value (large), delta vs window start (arrow + color, and a text sign for colorblind users), and the sparkline. Grid is responsive. Include a synthetic message generator so it runs standalone. Comment where the hot path is and why nothing there allocates.
LTTB over every-nth is underrated for live charts, every-nth eats exactly the spikes you built the dashboard to catch.async_annie 1 month ago
add a comment

2 Answers

14

The ring-buffer-outside-React pattern is the only thing that scales here. For the window question: derive it from each metric's observed message rate instead of hardcoding. Keep the window in TIME (e.g. last 60s) and let point count float; then LTTB down to exactly the pixel width. That way a 1Hz metric and a 100Hz metric both look right without per-metric tuning.

THE PROMPT
Make WINDOW time-based (seconds), not point-count-based. Track each metric's median inter-arrival time; size the ring buffer to hold window_seconds / median_interval * safety(1.5). Downsample to exactly the sparkline's pixel width with LTTB on each frame that changed.
10

Add a monotonic-clock guard: websocket timestamps can arrive out of order or with clock skew, and one bad future-dated point stretches the whole x-domain. I drop points whose t is more than a small epsilon ahead of the last, and log the count so I know if the source is misbehaving.

Your Answer