Skip to content
fearchitect
UI System-Design Patterns

Realtime Dashboard

Design a live data dashboard without overwhelming the main thread.

By Abas TurabliReviewed

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 angle

A 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

TransportDirectionReconnectInfra constraintDashboard fit
SSE (EventSource)Server → client onlyAutomatic + Last-Event-ID replayWorks behind any HTTP proxyBest: one-way price/sensor/metric feeds
WebSocketFull-duplexManual — write backoff logicNeeds sticky sessions or a brokerGood: user controls dashboard filters server-side
Short pollingClient-initiatedNone — new request each tickWorks everywhere; high overheadFallback 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.

SSE feed coalesced with requestAnimationFrametsx
"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.

Further reading

Search fearchitect

Jump to a topic, mode, or action.