Designing for sub-100ms: latency budgets in trading UIs
When a price moves, the screen has one job — show it before the trader feels the delay. A practical look at spending a latency budget across the wire, the framework, and the paint.
A trading interface is judged on a timescale most web apps never think about. By the time a React re-render feels "instant" to a marketing site, a market-data screen has already missed the window. The discipline that keeps these screens honest is the latency budget: a fixed amount of time — say 100 milliseconds from event to pixel — divided up and defended.
Where the milliseconds go
It helps to write the budget down as a ledger. A rough one for a wallet or markets view might read:
- Network / transport — 30ms. WebSocket frame in, deserialised, on the event loop.
- State & diffing — 20ms. Reconcile the update against what's already on screen.
- Render & layout — 30ms. Build the DOM changes, let the browser lay them out.
- Paint & compositing — 20ms. Actually put it on the glass.
Once it's a ledger, every architectural decision becomes a question of which line item pays for it.
The cheapest update is the one you don't make
The single biggest win is refusing to render. Markets push far more updates than a human eye can resolve, so coalescing is not optional:
// Collapse a burst of ticks into one paint per frame.
let pending = new Map<string, Tick>();
let scheduled = false;
function onTick(tick: Tick) {
pending.set(tick.symbol, tick); // last write wins per symbol
if (scheduled) return;
scheduled = true;
requestAnimationFrame(flush);
}
function flush() {
applyTicks(pending);
pending = new Map();
scheduled = false;
}
This one change turns an unbounded stream into at most one update per symbol per frame, and it hands the scheduling back to the browser at exactly the cadence the display can show.
A latency budget is really a forcing function. It converts "make it fast" — which no one can action — into "you have 20ms for state; spend it."
Measure event-to-paint, not function timing
Profiling individual functions tells you where time went, not whether the trader felt it. The number that matters spans from the inbound message to the next paint. Browsers expose enough to capture it honestly, and it's worth wiring a sampled version into production rather than trusting a clean local machine.
The teams that hit these numbers consistently aren't writing exotic code. They're being ruthless about what reaches the DOM, and they treat the budget as a number the whole team owns — not a thing the "performance person" fixes in a sprint at the end.