0306090120150180210240270300330km/h
0 km/h
ADIT JANGID

Inside a High-Frequency Trading Dashboard: React Performance Secrets

Uncover the React performance optimization techniques used to build a real-time trading dashboard handling 50,000+ ticks per second with zero lag and 60FPS animations.

Inside a High-Frequency Trading Dashboard: React Performance Secrets

Introduction

Building dynamic web dashboards for fintech or high-frequency trading (HFT) applications represents one of the toughest UI challenges in modern web engineering. When market tickers, order books, and depth charts update at sub-millisecond speeds (resulting in over 50,000 incoming updates per second), standard React state updates will freeze the browser.

By default, React's rendering model is designed for transactional updates (like clicks and page transitions). When forced to repaint the DOM hundreds of times a second, the main thread clogs up, drop-down menus stutter, and the browser tab crashes.

In this breakdown, we reveal the architectural patterns and React rendering secrets we used to build a real-time trading dashboard that handles 50,000+ ticks per second, preserving a buttery-smooth 60FPS user interface.


The Core Performance Challenge

React uses a Virtual DOM reconciliation process. When state changes:

  1. It reruns the component function.
  2. It compares the returned JSX structure with the previous one.
  3. It applies the difference (patch) to the real DOM.

If a tick arrives every 1ms, React performs this entire pipeline 1,000 times a second for every ticking widget. In Javascript, executing this process in a single thread blocks the UI event loop, rendering any mouse interaction or input completely unresponsive.

Our solution bypasses this pipeline entirely for high-frequency data.


System Architecture Overview

To achieve 60FPS under high loads, we split operations into separate threads and bypassed React state reconciliation for our ticking visual items:

[ WebSocket Server ]
         |
         | (High-frequency ProtoBuf / JSON)
         v
[ Web Worker Thread ]  <--- Decodes buffers, pools & batches ticks
         |
         | (Controlled postMessage batch updates at 16.6ms intervals)
         v
[ Main Thread ]
         |
         +-------> [ Zustand Micro-Store (Transient State) ]
         |
         +-------> [ HTML5 Canvas / Direct Ref Mutations ]

Secret 1: Offloading Parsing to Web Workers

Instead of running WebSockets directly on the browser's main thread, we offload connection parsing and data batching to a Web Worker. This keeps serialization overhead and JSON decoding operations isolated from the UI runtime.

Furthermore, we do not send every tick immediately to the main thread. Instead, we buffer updates inside the Worker and dispatch them at a controlled frame-rate interval (approx. 16.67ms, corresponding to 60Hz display refresh rate).

Here is the Web Worker script:

// worker.ts - runs in a separate background thread

let tickBuffer: any[] = [];
let socket: WebSocket | null = null;
const BATCH_INTERVAL_MS = 16.67; // 60 updates per second

self.onmessage = (e) => {
  if (e.data.action === 'CONNECT') {
    socket = new WebSocket(e.data.url);
    
    socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      tickBuffer.push(data);
    };

    // Start the batch execution loop
    setInterval(() => {
      if (tickBuffer.length > 0) {
        // Send the batch to the main thread
        self.postMessage({ type: 'TICK_BATCH', payload: tickBuffer });
        tickBuffer = []; // Clear buffer
      }
    }, BATCH_INTERVAL_MS);
  }
};

Secret 2: Canvas Hybrid Rendering

Rendering a dynamic table of order books using div elements is too expensive. Instead, we use an HTML5 <canvas> element for the ticking content, and standard React only for static labels, layout, and control buttons.

We manage the canvas draw loop inside a React ref, driven by the browser's native requestAnimationFrame loop.

import React, { useEffect, useRef } from 'react';

interface Tick {
  price: number;
  size: number;
  side: 'buy' | 'sell';
}

export const OrderBookCanvas: React.FC<{ worker: Worker }> = ({ worker }) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const dataRef = useRef<{ buys: Tick[]; sells: Tick[] }>({ buys: [], sells: [] });

  useEffect(() => {
    // Listen for batched ticks from our Web Worker
    const handleWorkerMessage = (e: MessageEvent) => {
      if (e.data.type === 'TICK_BATCH') {
        const batch: Tick[] = e.data.payload;
        
        // Merge the incoming updates into dataRef
        batch.forEach((tick) => {
          const target = tick.side === 'buy' ? dataRef.current.buys : dataRef.current.sells;
          target.unshift(tick);
          if (target.length > 20) target.pop(); // Keep only top 20 levels
        });
      }
    };

    worker.addEventListener('message', handleWorkerMessage);

    // Canvas drawing loop
    let animationFrameId: number;

    const draw = () => {
      const canvas = canvasRef.current;
      if (!canvas) return;
      
      const ctx = canvas.getContext('2d');
      if (!ctx) return;

      // Clear the canvas area
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = '#0f172a'; // Slate background
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      const { buys, sells } = dataRef.current;

      // Draw Buy Orders
      ctx.fillStyle = '#10b981'; // Green
      ctx.font = '12px monospace';
      buys.forEach((buy, i) => {
        ctx.fillText(`BUY  ${buy.price.toFixed(2)}  ${buy.size}`, 10, 20 + i * 16);
      });

      // Draw Sell Orders
      ctx.fillStyle = '#ef4444'; // Red
      sells.forEach((sell, i) => {
        ctx.fillText(`SELL ${sell.price.toFixed(2)}  ${sell.size}`, 220, 20 + i * 16);
      });

      animationFrameId = requestAnimationFrame(draw);
    };

    draw();

    return () => {
      worker.removeEventListener('message', handleWorkerMessage);
      cancelAnimationFrame(animationFrameId);
    };
  }, [worker]);

  return (
    <div className="border border-slate-700 rounded-lg overflow-hidden p-2 bg-slate-900">
      <h2 className="text-white text-sm font-semibold mb-2">Live Order Book</h2>
      <canvas ref={canvasRef} width={400} height={360} className="w-full h-auto" />
    </div>
  );
};

Because dataRef.current points to a mutable memory location and not a React state, updating it does not trigger any React re-renders. The canvas draws the current values on every display frame update, completely bypassing the virtual DOM.


Secret 3: Zustand Transient Updates

For pages where we absolutely must display a few dynamic values in text format (like the top header showing "Last Traded Price"), we use Zustand rather than React's built-in state.

Zustand allows us to write updates directly to the store without forcing a component to re-render. Instead, we subscribe to the changes transiently and mutate the inner text of a DOM node directly.

import { create } from 'zustand';

interface TickerStore {
  lastPrice: number;
  setLastPrice: (price: number) => void;
}

// 1. Create a Zustand store
export const useTickerStore = create<TickerStore>((set) => ({
  lastPrice: 0,
  setLastPrice: (price) => set({ lastPrice: price }),
}));

// 2. Component using a transient subscription
export const PriceTicker = () => {
  const priceRef = useRef<HTMLSpanElement | null>(null);

  useEffect(() => {
    // Subscribe to state changes without causing a component re-render
    const unsubscribe = useTickerStore.subscribe(
      (state) => state.lastPrice,
      (lastPrice) => {
        if (priceRef.current) {
          priceRef.current.innerText = `$${lastPrice.toFixed(2)}`;
          // Trigger a quick blink CSS animation class
          priceRef.current.classList.add('flash');
          setTimeout(() => priceRef.current?.classList.remove('flash'), 100);
        }
      }
    );

    return () => unsubscribe();
  }, []);

  return (
    <div className="p-4 bg-slate-800 rounded-lg">
      <span className="text-slate-400 text-xs uppercase block">Last Traded Price</span>
      <span ref={priceRef} className="text-2xl font-bold text-white">$-.--</span>
    </div>
  );
};

Conclusion & Performance Checklist

By shifting our architecture toward threaded message parsing, direct ref canvas rendering, and transient subscriptions, we eliminated layout recalculations.

If you are building dashboards handling massive streams of data:

  1. Never map dynamic lists directly to arrays inside React state if they update faster than 100ms.
  2. Buffer incoming socket ticks in Web Workers to match the display update rate.
  3. Keep the browser main thread clear for user input operations by using Canvas elements for high-frequency drawing tasks.
reactperformanceweb-socketscanvasfintech

Related Posts

Interested in working together?

Let's translate complex software and automation problems into clean, high-performance systems.