← Back to blog

Latency Budgets for Trading UIs: Where Your Milliseconds Go

Profiling the render pipeline from WebSocket message to screen update - identifying bottlenecks, setting budgets, and measuring what matters.

Oliver Benns
Oliver Benns

Software engineer · Creator of Hedge UI


When a price changes on an exchange, there is a measurable delay before a trader sees that change on their screen. For most web applications, nobody cares whether that delay is 20ms or 200ms. For trading applications, the difference matters. Stale prices lead to bad decisions, and a UI that visibly lags behind the market erodes trust.

Understanding where those milliseconds go is the first step to reducing them.

The pipeline

A price update travels through several stages before reaching the screen:

  1. Exchange to server - Network latency to your backend (or directly to the exchange)
  2. Server processing - Your backend receives, validates, and re-broadcasts the update
  3. Server to browser - WebSocket message delivery over the user's connection
  4. Message parsing - JSON.parse or binary decoding in the browser
  5. State update - Writing the new value into your state management layer
  6. React reconciliation - Diffing the virtual DOM and computing the update
  7. DOM mutation - Applying changes to the actual DOM
  8. Paint and composite - The browser rendering the pixels to the screen

Stages 1-3 are network latency and largely outside your control on the frontend. Stages 4-8 are where frontend engineering makes the difference.

Measuring end-to-end latency

Add timestamps at each stage to see where time is spent:

function measureUpdateLatency(message: WebSocketMessage) { const stages = { serverTimestamp: message.timestamp, received: performance.now(), parsed: 0, stateUpdated: 0, rendered: 0, }; // After parsing stages.parsed = performance.now(); // After state update requestAnimationFrame(() => { stages.stateUpdated = performance.now(); // After paint requestAnimationFrame(() => { stages.rendered = performance.now(); logLatency({ parsing: stages.parsed - stages.received, stateUpdate: stages.stateUpdated - stages.parsed, render: stages.rendered - stages.stateUpdated, total: stages.rendered - stages.received, }); }); }); }

Run this in development with a realistic data feed. The numbers will tell you exactly where to focus your optimisation effort.

Setting a budget

A reasonable latency budget for each frontend stage:

StageBudgetNotes
Message parsing< 1msJSON.parse is fast for small messages
State update< 2msDepends on state management approach
React reconciliation< 5msThe biggest variable
DOM mutation + paint< 3msBrowser-dependent
Total frontend< 11msLeaves headroom within a 16ms frame

The 16ms frame budget comes from 60fps rendering. If your total frontend processing exceeds 16ms, you will drop frames and the UI will feel sluggish.

Message parsing

JSON parsing is rarely the bottleneck for individual messages, but it adds up under high message rates. If you are processing 500+ messages per second, consider:

Binary protocols - MessagePack or Protocol Buffers are faster to decode than JSON. The savings are small per message but compound at scale.

Selective parsing - If you only need the price field from a large message, parse only what you need:

// Slow: parse everything const data = JSON.parse(message.data); const price = data.price; // Faster for large messages: extract the field function extractPrice(raw: string): number { const match = raw.match(/"price":(\d+\.?\d*)/); return match ? parseFloat(match[1]) : NaN; }

In practice, this optimisation is only worth it if profiling shows parsing as a bottleneck. For most trading UIs, it is not.

State update cost

The state management layer is often where latency hides. Common issues:

Immutable updates creating garbage - Every { ...state, quotes: { ...state.quotes, [symbol]: newQuote } } creates new objects that need garbage collection. Under high update rates, GC pauses cause visible jank.

// This creates garbage on every update set((state) => ({ quotes: { ...state.quotes, [symbol]: newQuote }, })); // Mutating a ref and flushing periodically avoids this const quotesRef = useRef(new Map<string, Quote>()); quotesRef.current.set(symbol, newQuote);

Store subscriptions firing broadly - If your entire quote store updates on every tick, every subscribed component re-evaluates its selector. Use fine-grained subscriptions:

// Bad: subscribes to the entire quotes object const quotes = useStore((state) => state.quotes); // Good: subscribes only to the specific symbol const btcQuote = useStore((state) => state.quotes["BTC-USD"]);

React reconciliation

React reconciliation is typically the largest chunk of frontend latency. Reduce it by:

Limiting the re-render scope - Only the components that display the changed data should re-render. Use React.memo, selectors, or the React Compiler to prevent cascading updates.

Virtualising long lists - An order book with 50 visible rows should only render 50 rows, not 500. Libraries like @tanstack/react-virtual handle this well.

Batching updates - React 18+ automatically batches state updates within event handlers and effects. For WebSocket callbacks, wrap updates in ReactDOM.flushSync only if you need synchronous rendering (you rarely do).

One micro-optimisation worth mentioning: our order book depth bars use CSS transform: scale3d(width, 1, 1) instead of setting a percentage width. Transforms skip layout and go straight to composite, so updating ten depth bars per frame does not trigger a reflow. Small detail, but it keeps the order book's per-frame cost well within the 3ms DOM budget.

The RAF buffer pattern

The single most effective optimisation for high-frequency trading UIs is the requestAnimationFrame buffer. Instead of updating state on every message, collect messages in a mutable buffer and flush once per frame:

const buffer = new Map<string, Quote>(); let rafScheduled = false; function onMessage(quote: Quote) { buffer.set(quote.symbol, quote); if (!rafScheduled) { rafScheduled = true; requestAnimationFrame(() => { // Flush all buffered updates in a single state update const updates = new Map(buffer); buffer.clear(); rafScheduled = false; applyQuoteUpdates(updates); }); } }

This collapses potentially hundreds of messages into one state update per frame. For an order book receiving 200 updates per second, this reduces 200 state updates and re-renders to 60.

Profiling with Chrome DevTools

The Performance tab in Chrome DevTools is your primary tool. Record a session with a live data feed and look for:

  • Long tasks (yellow bars > 50ms) - These cause visible lag. Drill into the call stack to find the culprit.
  • Layout thrashing - Repeated forced reflows from reading layout properties between writes.
  • GC events - Frequent garbage collection pauses indicate too much object allocation.
  • Paint storms - Large areas being repainted when only a small region changed.

Use performance.mark and performance.measure to add custom markers:

performance.mark("quote-received"); // ... process and render ... performance.mark("quote-rendered"); performance.measure("quote-pipeline", "quote-received", "quote-rendered");

These appear in the DevTools timeline and let you track your pipeline latency over time.

Continuous monitoring

Do not just profile once. Set up continuous latency monitoring that runs in production:

function reportRenderLatency(component: string, durationMs: number) { if (durationMs > 16) { analytics.track("slow_render", { component, duration: durationMs, userAgent: navigator.userAgent, timestamp: Date.now(), }); } }

Track p50, p95, and p99 render latencies. A UI that is fast on average but occasionally spikes to 200ms is still a bad experience.

The bottom line

Most trading UI latency is not in the network. It is in the frontend pipeline - state management, React reconciliation, and DOM updates. Setting explicit budgets for each stage, measuring against them, and optimising the bottlenecks is how you build an interface that feels instantaneous even during volatile markets.

Kickstart Your Trading Application

Hedge UI is a React starter kit with production-ready trading components, real-time data handling, and customisable layouts — so you can ship faster.

Get Hedge UI