patterntypescriptModerate
Real-Time Analytics Dashboards Must Batch Updates to Prevent DOM Thrashing
Viewed 0 times
real-time analyticsrequestAnimationFramebatchingDOM thrashingdashboard performancerAFmetric updates
Problem
A real-time analytics dashboard receiving dozens of metric updates per second re-renders on each WebSocket message, causing the UI to freeze as the DOM is updated far faster than the browser's 60fps frame budget.
Solution
Accumulate incoming updates in a buffer and flush them on the next animation frame. This limits UI updates to 60fps regardless of data arrival rate.
const updateBuffer = new Map<string, MetricUpdate>();
let rafId: number | null = null;
function enqueueUpdate(update: MetricUpdate) {
updateBuffer.set(update.metricId, update); // latest value per metric
if (rafId === null) {
rafId = requestAnimationFrame(() => {
const updates = new Map(updateBuffer);
updateBuffer.clear();
rafId = null;
applyUpdates(updates);
});
}
}
ws.onmessage = (event) => {
const update = JSON.parse(event.data) as MetricUpdate;
enqueueUpdate(update);
};Why
requestAnimationFrame fires at the display refresh rate (60–120Hz). Batching into it means at most one render per frame, discarding intermediate values that would never be visible anyway. This is the same technique React's concurrent mode uses internally.
Gotchas
- The Map keyed by metricId means only the latest value per metric is applied — fine for gauges, wrong for event logs.
- If the tab is in the background, rAF fires at 1fps — use setInterval(16) for background tabs if freshness matters.
- For React, useReducer + useSyncExternalStore with rAF batching prevents tearing better than useState.
- Charts (D3, Chart.js) have their own render cycles — debounce chart data updates separately from DOM metrics.
Revisions (0)
No revisions yet.