Summary
A realtime dashboard streams high-frequency data from the server and renders it efficiently without dropping frames. The key decisions are transport choice (SSE vs WebSocket vs polling), batching updates with requestAnimationFrame to coalesce renders, dropping stale frames under backpressure, reconnecting with exponential backoff, and picking canvas over SVG/DOM for dense data.
Jump to the interview angleA realtime dashboard maintains a persistent server connection, receives a stream of data events, and repaints a UI at display frame rate (60–120 fps). The hard part is not the network — it's preventing incoming data from triggering a repaint on every message. At 100 msg/s, naively calling setState or updating the DOM on each event causes frame drops and jank. The solution is to accumulate state in a mutable buffer and flush it once per animation frame via requestAnimationFrame.
Transport choice
| Transport | Direction | Reconnect | Infra constraint | Dashboard fit | |
|---|---|---|---|---|---|
| SSE (EventSource) | Server → client only | Automatic + Last-Event-ID replay | Works behind any HTTP proxy | Best: one-way price/sensor/metric feeds | |
| WebSocket | Full-duplex | Manual — write backoff logic | Needs sticky sessions or a broker | Good: user controls dashboard filters server-side | |
| Short polling | Client-initiated | None — new request each tick | Works everywhere; high overhead | Fallback for low-frequency or proxy-blocked environments |
rAF-coalesced update loop
Buffer incoming messages in a ref; flush the buffer inside a requestAnimationFrame callback. This limits React re-renders to 60 per second regardless of message rate.
"use client";
import { useEffect, useRef, useState } from "react";
interface Metric { id: string; value: number }
export function MetricsDashboard({ url }: { url: string }) {
const [metrics, setMetrics] = useState<Record<string, number>>({});
const bufferRef = useRef<Metric[]>([]);
const rafRef = useRef<number | null>(null);
useEffect(() => {
const es = new EventSource(url, { withCredentials: true });
es.addEventListener("message", (e) => {
// Accumulate; never setState here
bufferRef.current.push(JSON.parse(e.data) as Metric);
if (rafRef.current === null) {
rafRef.current = requestAnimationFrame(() => {
// Drain buffer: last value per id wins (drop stale frames)
const patch: Record<string, number> = {};
for (const m of bufferRef.current) patch[m.id] = m.value;
bufferRef.current = [];
rafRef.current = null;
setMetrics((prev) => ({ ...prev, ...patch }));
});
}
});
return () => {
es.close();
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
};
}, [url]);
return (
<ul>
{Object.entries(metrics).map(([id, val]) => (
<li key={id}>{id}: {val}</li>
))}
</ul>
);
}The rAF gate ensures at most one setState per frame. Taking the last value per id drops stale intermediate frames under backpressure.
Reconnection and resync
- On disconnect, reconnect with exponential backoff — start at 1 s, cap at 30 s.
- SSE sends Last-Event-ID automatically; WebSocket needs a manual cursor or sequence number.
- After reconnect, fetch a snapshot endpoint first, then resume the stream to avoid gaps.
- Reset backoff delay to 1 s on the first successful message, not on connection open.
- Distinguish network loss from server-side close — only backoff on transient errors.
Canvas vs SVG/DOM for dense data
- DOM/SVG: each data point is a node — fine up to ~500 points, degrades past ~2 000.
- Canvas 2D: draw 50 000+ points per frame imperatively; no layout cost.
- SVG retains interactivity (hover, click) per node; canvas needs manual hit-testing.
- Use OffscreenCanvas + a Web Worker to move chart rendering fully off the main thread.
- Hybrid: SVG overlay on top of canvas for interactive annotations on a dense chart.
Do not coalesce with setInterval
A fixed-interval flush (e.g. every 16 ms) can fire mid-paint and cause tearing. requestAnimationFrame fires before the browser composites the frame, making it the correct flush point. Also avoid storing incoming data in React state — buffer it in a ref so accumulation never triggers a render.
Interview angle
Interviewers want to hear you separate concerns: transport choice is about directionality and infra constraints; render strategy is about frame budget. Name requestAnimationFrame coalescing and canvas vs DOM as distinct decisions.
Soundbite: "SSE for one-way push, rAF to coalesce, canvas when node count exceeds DOM budget."
Key terms
- requestAnimationFrame (rAF)
- Browser API that schedules a callback before the next paint, used to coalesce high-frequency updates into one render per frame.
- Backpressure
- When the producer sends data faster than the consumer can render it; handle by dropping stale frames.
- Coalescing
- Merging multiple incoming events into one state update per frame so the DOM or canvas paints once instead of many times.
- EventSource
- Browser API for SSE: a persistent GET that fires `message` events and auto-reconnects with `Last-Event-ID`.
- Canvas 2D API
- Imperative drawing API that renders thousands of data points in microseconds without creating DOM nodes.