In a trading application, performance is not a nice-to-have — it is the product. A 200ms render lag during a market spike is the difference between a filled order and a missed trade. Yet many React trading UIs ship with the same performance anti-patterns that would be acceptable in a dashboard or e-commerce site, without realising how badly those patterns compound under real-time data pressure.
This guide covers the five highest-leverage optimisations for React trading applications, with concrete patterns you can apply today.
1. Throttle and batch real-time data before it hits React state
A WebSocket feed for a single liquid instrument can emit 50–500 price updates per second. If each message triggers a setState call, React will attempt to reconcile and re-render at that frequency — far exceeding what the browser can paint and far more than a user can perceive.
How to implement:
- Accumulate incoming messages in a mutable buffer (a plain
refor module-level variable) outside of React state. - Use
setIntervalorrequestAnimationFrameto flush the buffer into state at a controlled rate (typically 60ms for display, or matching your target frame rate). - For order book data specifically, merge updates in the buffer before committing to state so React only ever sees the final, collapsed snapshot.
const buffer = useRef<Map<string, Quote>>(new Map());
useEffect(() => {
const flush = setInterval(() => {
if (buffer.current.size === 0) return;
setQuotes(prev => new Map([...prev, ...buffer.current]));
buffer.current.clear();
}, 60);
return () => clearInterval(flush);
}, []);
socket.onmessage = (event) => {
const quote: Quote = JSON.parse(event.data);
buffer.current.set(quote.symbol, quote);
};
React 18's automatic batching helps but does not solve the root problem. Controlling the rate at which data enters state is always more effective than relying on the scheduler to handle it after the fact.
2. Virtualise every long list and grid
An order book has hundreds of price levels. A trade blotter can hold thousands of rows. Rendering all of these as DOM nodes simultaneously will destroy scroll performance and cause layout thrashing, regardless of how efficiently React diffs the virtual DOM.
Use @tanstack/react-virtual or react-window for any list or grid that can exceed 50–100 rows. For order books, prefer a fixed-size virtualiser — rows are uniform in height and the list length is predictable.
const rowVirtualizer = useVirtualizer({
count: orderBook.asks.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 28,
overscan: 10,
});
Pair virtualisation with React.memo on row components to prevent re-renders of rows that did not change in the latest flush.
3. Move heavy computation off the main thread with Web Workers
Technical indicator calculations (VWAP, RSI, Bollinger Bands), order book aggregation across price levels, and large dataset transformations are CPU-intensive. Running these on the main thread blocks rendering. In a trading context, a frozen UI during a volatile market period is a critical failure.
Use the native Worker API or the comlink library to offload computation:
// indicator.worker.ts
self.onmessage = ({ data: { ticks } }) => {
const result = computeRSI(ticks, 14);
self.postMessage(result);
};
// Component
const workerRef = useRef<Worker>();
useEffect(() => {
workerRef.current = new Worker(
new URL('./indicator.worker.ts', import.meta.url)
);
workerRef.current.onmessage = ({ data }) => setRsi(data);
return () => workerRef.current?.terminate();
}, []);
Virtualisation (tip 2) reduces rendering work — the number of DOM nodes the browser must paint. Web Workers reduce computation work — moving expensive JavaScript off the main thread. They operate at different layers and are complementary, not contradictory.
4. Architect state to minimise re-render blast radius
A single global context or Redux slice holding live prices, positions, orders, and UI state means any price tick re-renders every subscribed component. In a trading app, that is effectively your entire UI on every tick.
Adopt an atom-based state library — Zustand or Jotai. These allow components to subscribe to exactly the slice of state they need.
const usePriceStore = create<PriceState>(...); // updates ~60fps
const usePositionStore = create<PositionState>(...); // updates on fills only
const useAccountStore = create<AccountState>(...); // updates infrequently
// Component only re-renders when its specific symbol changes
const bid = usePriceStore(state => state.quotes[symbol]?.bid);
Avoid React Context for any data that updates more than a few times per second. Context re-renders every consumer on every update, with no built-in selector support.
5. Memoise aggressively and measure before you optimise
React.memo, useMemo, and useCallback all have overhead. Used indiscriminately they add cost without benefit. Used correctly in a trading app — where the same components receive rapid, identical updates — they eliminate enormous amounts of wasted work.
- Wrap all leaf components that receive data from the high-frequency price store in
React.memowith a custom comparison function where needed. - Use
useMemofor computed values that are expensive and depend on infrequently-changing inputs (e.g., aggregated P&L across positions). - Before adding any memo, profile first using the React DevTools Profiler or the
why-did-you-renderlibrary.
const PositionRow = React.memo(({ position }: Props) => {
const unrealizedPnl = useMemo(
() => calculateUnrealizedPnl(position),
[position.quantity, position.avgPrice, position.currentPrice]
);
return <tr>...</tr>;
}, (prev, next) =>
prev.position.id === next.position.id &&
prev.position.currentPrice === next.position.currentPrice
);
Reference equality is the most common cause of React.memo failing silently. If a parent recreates an object or array literal on every render and passes it as a prop, memo will never bail out.
Bringing it all together
| Layer | Optimisation |
|---|---|
| Data ingestion | Throttle and batch WebSocket updates |
| Rendering | Virtualise long lists and grids |
| Computation | Offload heavy work to Web Workers |
| State architecture | Minimise re-render blast radius with atoms |
| Component efficiency | Memoise strategically with measurement |
Applied together, these allow React trading UIs to handle real-time data at market frequencies without degrading the user experience. The key discipline is to measure before optimising — React DevTools Profiler and browser performance traces will show you exactly where time is being spent before you commit to architectural changes.