New: We've launched a brand new Coding Challenges section! Check out these interactive, real-world exercises to level up your skills.Explore Challenges
FrontendPrep
system-designHard

Design a Real-Time Analytics Dashboard

Loading...

Master the frontend system design of a real-time analytics dashboard. Learn about layout positioning, data sync architectures (polling vs. SSE vs. WebSockets), chart performance, and high-frequency telemetry state batching.

Arvind M
Arvind MLinkedIn

Frontend System Design Master Guide

This question is part of our comprehensive Frontend System Design Series. Master the complete framework, requirements, and edge cases before diving in.

Read Guide

Design a Real-Time Analytics Dashboard

Real-time analytics dashboards are highly popular frontend system design questions (often asked by companies like Datadog, Uber, Stripe, and Vercel). They require you to coordinate multiple challenging domains: complex responsive layouts, efficient streaming network protocols, client-side data buffering, high-performance rendering (SVG vs. Canvas), and rendering optimizations to prevent UI freeze under heavy throughput.


1. What the Interviewer Is Testing

When an interviewer asks you this question, they are evaluating your capabilities as an application architect. They want to see how you balance rich features (interactivity, customizability) with strict system performance:

  • Dynamic Layout & Personalization: Can you build a grid system where users can drag, drop, resize, and configure widgets without lagging? How is layout configuration state saved and loaded?
  • Data Ingestion & Syncing Protocols: How do you sync data between the server and the UI? When do you choose WebSockets vs. Server-Sent Events (SSE) vs. HTTP Polling?
  • Preventing Rendering Bottlenecks (React Churn): If your backend sends 100 telemetry updates per second, does the UI freeze? How do you prevent React from executing useless reconciliation runs?
  • Data Visualization Performance: When should you render charts using SVG vs. HTML5 Canvas vs. WebGL?
  • Code Splitting & Bundle Size: Charting libraries (like D3, ECharts, or Chart.js) are heavy. How do you prevent them from bloating the initial page load time?

2. How to Open Your Answer (The First 2 Minutes)

Establish the scope of the system immediately by asking clarifying questions. This sets up your design parameters and shows senior engineering leadership.

Say something like:

*"Before I design the architecture, I'd like to clarify a few key product and technical requirements.

First, is the dashboard layout static, or can users customize it? I will assume users can customize their layouts—adding, removing, dragging, and resizing cards—and that we need to serialize and save these configurations.

Second, what is the frequency and size of incoming data? Are we building a high-frequency financial telemetry chart (updates every 50ms) or a business intelligence dashboard (updates every few minutes)? I will assume we are dealing with high-frequency telemetry data streams (updates every 100-200ms) to ensure we address real-time rendering constraints.

Third, do we require historical date-range querying along with the real-time streams? I'll design the system to support historical query fetches alongside live socket streams.

Finally, are there any viewport limits? I will design desktop-first since power users utilize dashboards on larger screens, but with a responsive layout engine that gracefully collapses to a single-column layout on mobile viewports."*


3. The 6-Step Answer Framework

Walk through this structured framework to systematically design a robust dashboard system.

Step 1: Requirements Gathering (Functional & Non-Functional)

  • Functional Requirements:
    • Dynamic grid layout where widgets can be dragged, dropped, resized, added, and configured.
    • Support for diverse widget types: Metric Cards (KPI numbers), Line Charts (telemetry streams), Bar Charts, and Real-Time Event Tables.
    • A global time-range selector (e.g., Last 15 min, Last 24 hours, Custom Range) that updates all widget queries.
    • Real-time streaming data updates alongside historical queries.
  • Non-Functional Requirements:
    • 60 FPS UI Rendering: The main thread must remain unblocked, and scroll actions must remain smooth even when charts are rendering hundreds of updates per second.
    • Fast Initial Page Load: Initial bundle size must be optimized (e.g., FCP < 1.5s) by lazy-loading heavy visualization components.
    • Resilience: Clean reconnect logic with exponential backoff and jitter if the real-time connection drops.
    • Bandwidth & Battery Efficiency: Prevent high-frequency background updates from draining mobile batteries or hogging bandwidth.

Step 2: Component & UI Architecture

Decompose the dashboard application into logical containers and modules:

Component & UI Architecture: Real-Time Analytics Dashboard

  • Dashboard Grid Engine: A dynamic CSS grid coordinator that parses layout JSON configurations and handles grid positioning, dragging, and resizing boundaries.
  • Widget Container: A wrapping component that encapsulates widget settings, loading states, error states, and edit menus.
  • Widget Renderer: A dynamic loader that decides which visualization type to render based on widget configuration, handling lazy-loading of charting engines.
  • Data Sync Provider: A React Context provider that wraps the dashboard. It manages a centralized WebSockets/SSE connection, multiplexes queries, and delegates received streaming data to respective widgets using an event emitter pattern.
  • State Model:
    • layout: An array of grid widget placements.
    • widgetsConfig: Record mapping widget IDs to their configurations (e.g., metric names, chart types, custom thresholds).
    • timeRange: The active dashboard time window (e.g., { start: 1719230000, end: 1719230900 }).

Step 3: State Management & Caching

Real-time dashboards require an optimized state architecture to avoid layout thrashing and CPU bottlenecks:

  • Dynamic Layout Configuration State:
    • Represent layout using a flat array schema:
      interface LayoutItem {
        id: string;
        x: number; // Grid column offset
        y: number; // Grid row offset
        w: number; // Grid width (in columns)
        h: number; // Grid height (in rows)
        type: string; // Widget type (e.g. 'line-chart', 'metric-card')
      }
    • This JSON structure is easy to store in databases and synchronizes efficiently.
  • Multiplexed Connection (Single Connection):
    • Do NOT open a separate WebSocket or SSE connection per widget. A browser has a limit of concurrent HTTP connections (usually 6 per domain for HTTP/1.1), and spinning up multiple connections wastes server resources.
    • Open a single connection wrapper. The DataSyncProvider sends a single subscription request containing all active widget metric topics. The server then multiplexes the metrics into a unified event stream, which the client separates and routes to individual widgets.
  • Sliding Window Buffer (Client Cache):
    • Keep a memory buffer of the last $N$ points (e.g., last 100 telemetry points or last 10 minutes) inside a mutable ref or memory store.
    • When new stream frames arrive, append them to the buffer, delete obsolete points that have drifted out of the active window, and schedule a batch UI render.

Step 4: Data Fetching & Sync Protocol

We must select the correct protocol for different data paths:

FeatureHTTP PollingServer-Sent Events (SSE)WebSockets
DirectionClient $\rightarrow$ ServerServer $\rightarrow$ ClientBidirectional (Duplex)
TransportStandard HTTPHTTP/2 (Persistent)TCP upgrade
OverheadHigh (Headers on every poll)LowLow
ReconnectionsAutomatic (per request)Built-in automatic retryCustom JS logic required
Best Used ForHistorical loads, static updatesTelemetry feeds, event feedsInteractive collabs, chat

Protocol Recommendations

  1. Historical & Layout Configuration Data: Use standard REST/GraphQL over HTTP.
  2. Telemetry Stream Data: Use Server-Sent Events (SSE). Dashboards are primarily downstream read-only consumers. SSE is lighter than WebSockets, supports multiplexing out-of-the-box over HTTP/2, and features built-in browser-level auto-reconnection logic. If bidirectional features are needed (like dynamic interactive chat widgets), upgrade to WebSockets.

API Schema Contract

WebSocket/SSE Metric Subscription Payload (Client to Server):

{
  "action": "SUBSCRIBE",
  "topics": ["cpu-utilization", "memory-load", "network-in"],
  "windowSeconds": 600
}

Real-Time Data Feed Payload (Server to Client):

{
  "topic": "cpu-utilization",
  "timestamp": 1719230910,
  "value": 42.7,
  "metadata": { "hostId": "server-us-east-1" }
}

Step 5: Performance Considerations

Performance optimizations make or break dashboard architectures:

1. Preventing Render Storms (Batching Updates)

  • The Problem: If telemetry updates arrive every 10ms, calling React setState directly on every socket message triggers continuous re-renders. The browser main thread freezes and inputs lag.
  • The Solution: Implement a Throttle & Batch Buffer. Store incoming updates in a mutable Javascript array ref (useRef). Use a timer or requestAnimationFrame to flush the buffered updates into the React state at a throttled interval (e.g., every 150ms-250ms). This caps render passes to a maximum of 4-6 times per second, which is visually instantaneous to users but preserves CPU.

2. Chart Rendering Choice: SVG vs. Canvas

  • SVG (Scalable Vector Graphics):
    • Pros: DOM-based, styleable with CSS, handles hover/click interactions easily using standard DOM event listeners.
    • Cons: High memory consumption when rendering thousands of data points (every point is an actual DOM node).
    • Best for: Metric cards, simple bar charts, charts with small historical datasets (< 500 nodes).
  • Canvas (HTML5):
    • Pros: Fast, lightweight raster-based rendering. Can draw millions of points effortlessly on a single canvas element.
    • Cons: Retained mode pixel-based layout. You must manually calculate hover positions (e.g. mouse coordinates relative to nodes) to show tooltips.
    • Best for: Dense real-time line graphs, high-frequency scatters, complex graphs (> 1000 nodes).

3. Dynamic Code Splitting

  • Use React.lazy() and Suspense to code-split heavy chart packages (like D3, ECharts, Recharts) out of the main landing bundle. Dashboards should load layout containers instantly and stream chart libraries in the background.

Step 6: Accessibility & Edge Cases

  • Tabular Data Fallbacks: Screen readers cannot read graphical canvas charts. Provide a hidden screen-reader-only HTML data table adjacent to the chart, containing the raw metrics summary:
    <div class="sr-only" aria-live="polite">
      <h3>CPU Utilization Summary</h3>
      <table>
        ...
      </table>
    </div>
  • Keyboard Navigation: Allow users to focus widgets using the Tab key. Support moving/rearranging widgets via arrow key commands.
  • Tab Backgrounding (Page Visibility): When the page is hidden (e.g. tab is inactive), pause WebSocket/SSE subscriptions or drop incoming packets. Resume fetches when document.visibilityState === 'visible'. This prevents battery drain.
  • WebSocket Reconnect Storms: If your WebSocket server goes down, thousands of dashboards will try to reconnect simultaneously. To prevent crashing your load balancers, implement Exponential Backoff with Jitter on client reconnects:
    Delay = (2 ^ retries) * 1000ms + RandomJitter

4. What Candidates Miss

Show senior engineering depth by explicitly pointing out these real-world gotchas:

  1. Opening Too Many Sockets: Not sharing a single SSE/WS channel. Warn against rendering 10 widgets that open 10 socket connections.
  2. Chart Garbage Collection (Memory Leaks): Forgetting to call chart destruction functions (e.g., chart.destroy()) inside React cleanup hooks (useEffect). High-frequency charts create massive objects; failing to clean them up will crash the tab after 10 minutes.
  3. Resize Layout Thrashing: Dynamically rendering canvas charts on container resize can cause layout loops. If grid resizing updates container size, which triggers canvas redraw, which changes container scrollbars, which changes size again—you get infinite rendering loops.
    • Fix: Use ResizeObserver wrapped with a debounce function.

5. Code Implementation

Here are the key implementations to show in your interview:

1. High-Frequency Telemetry Batching Hook

This React hook buffers incoming high-frequency data updates and flushes them to the React component state in batches to prevent render thrashing:

import { useEffect, useRef, useState } from "react";
 
interface TelemetryPoint {
  timestamp: number;
  value: number;
}
 
export function useTelemetryBatch(
  metricTopic: string,
  eventEmitter: any, // Shared EventEmitter instance from DataSyncProvider
  windowLimit = 50,
  flushIntervalMs = 200,
) {
  const [dataPoints, setDataPoints] = useState<TelemetryPoint[]>([]);
 
  // 1. Maintain a mutable buffer to accumulate items without triggering renders
  const bufferRef = useRef<TelemetryPoint[]>([]);
  const stateRef = useRef<TelemetryPoint[]>([]);
 
  useEffect(() => {
    stateRef.current = dataPoints;
  }, [dataPoints]);
 
  useEffect(() => {
    // 2. Listen to shared incoming metric stream events
    const handleNewPoint = (point: TelemetryPoint) => {
      bufferRef.current.push(point);
    };
 
    eventEmitter.on(metricTopic, handleNewPoint);
 
    // 3. Periodic timer to batch update the state
    const timer = setInterval(() => {
      if (bufferRef.current.length === 0) return;
 
      // Extract new items and clear buffer
      const newPoints = [...bufferRef.current];
      bufferRef.current = [];
 
      // Merge with previous state and slice sliding window limit
      setDataPoints((prevData) => {
        const merged = [...prevData, ...newPoints];
        if (merged.length > windowLimit) {
          return merged.slice(merged.length - windowLimit);
        }
        return merged;
      });
    }, flushIntervalMs);
 
    return () => {
      eventEmitter.off(metricTopic, handleNewPoint);
      clearInterval(timer);
    };
  }, [metricTopic, eventEmitter, windowLimit, flushIntervalMs]);
 
  return dataPoints;
}

2. Code-Split Chart Component Wrapper

This component dynamically loads heavy charting engines, ensuring the main application bundle remains small and loads quickly:

import React, { Suspense, lazy } from "react";
 
// 1. Dynamically import the heavy Chart component
const HeavyLineChart = lazy(() =>
  import("./HeavyLineChart").then((module) => ({
    default: module.HeavyLineChart,
  })),
);
 
interface LazyChartProps {
  widgetId: string;
  metricTopic: string;
  chartOptions: Record<string, any>;
}
 
export function LazyChart(props: LazyChartProps) {
  return (
    <div className="w-full h-64 relative border border-zinc-800 bg-zinc-950 rounded-lg p-4">
      {/* 2. Wrap with Suspense to show fallback loading indicators during fetch */}
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyLineChart {...props} />
      </Suspense>
    </div>
  );
}
 
function ChartSkeleton() {
  return (
    <div className="w-full h-full flex flex-col justify-between animate-pulse">
      <div className="h-6 bg-zinc-800 w-1/3 rounded" />
      <div className="flex-1 flex items-end gap-2 mt-4">
        <div className="h-[20%] bg-zinc-800 w-full rounded" />
        <div className="h-[60%] bg-zinc-800 w-full rounded" />
        <div className="h-[40%] bg-zinc-800 w-full rounded" />
        <div className="h-[80%] bg-zinc-800 w-full rounded" />
      </div>
    </div>
  );
}

6. Summary & Key Takeaways

  • Multiplex Data Streams: Share a single connection (SSE/WebSockets) for all dynamic widget data to reduce socket initialization overhead.
  • Batch Render Changes: Never update React state directly on socket event frames. Buffer incoming data in refs and flush in batches (e.g., every 150-200ms) to ensure smooth 60fps scrolling and UI navigation.
  • Visual Strategy: Choose SVG for interactive charts with fewer coordinates, and Canvas for real-time visualization of dense metrics.
  • Lazy Load Libraries: Charting engines are heavy; keep landing bundles clean by dynamic importing (React.lazy()).
  • Clean Up Memory: Always call chart destruction and unsubscribe callbacks inside component unmount functions to avoid leaking memory.

Finished practicing this challenge?

Mark it as completed to track your progress, or bookmark it to review later.

Loading...

Share this Resource

Help other developers level up by sharing this study guide.

⚡ Weekly newsletter

Crack Your Next Frontend Interview.

Join senior engineers who receive practical, deep-dive frontend challenges, detailed concepts, and blueprints directly in their inbox.

  • Senior level React, JS, and CSS interview blueprints
  • System Design & performance optimization deep-dives
  • 100% free, zero spam, unsubscribe with one click

Join the Study Track

We value your privacy. Unsubscribe at any time.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.