The dashboard opened in review: a live table fed by WebSockets, one message per state change, one setState per message. Every device on the network was fine. Every laptop fan was spinning.
The instinct that built it — "real-time data means real-time rendering" — feels correct and scales terribly. Receiving events immediately does not require repainting every part of the UI immediately.
Transport frequency ≠ rendering frequency
The socket can deliver 50 events per second while the interface updates twice. Nothing is lost, because the data is real-time and the pixels are batched. Users can't perceive 50 frames of table changes — they can absolutely perceive the jank caused by trying to draw them.
The architecture that worked
- Snapshot on connect. One API call establishes complete state; the socket only carries deltas after that. Reconnects re-snapshot instead of replaying a fragile event history.
- Batch high-frequency events. Buffer deltas in the store, flush to React on a fixed interval (250–500ms for tables, per-frame only for something like a price ticker the user is staring at).
- Subscribe per active view. A chart subscribes to its series; a table subscribes to its page. Off-screen data updates the store silently — no re-render, no reconciliation cost.
Different update policies for different surfaces
| Surface | Policy |
|---|---|
| Counters/KPIs | Batched, 500ms–1s |
| Tables | Batched with row-level memoization — only changed rows re-render |
| Charts | Batched, redraw on flush or on visibility |
| Tickers the user watches | Per-event, isolated in its own component |
The key is that the policy is a decision per surface, not a global default inherited from how the socket happens to be wired.
The unglamorous half: reconciliation
Real systems reconnect. The store must handle:
- Ordering — sequence numbers per entity, so an out-of-order delta doesn't resurrect stale state
- Deduplication — the same event arriving twice (at-least-once delivery)
- Reconciliation — on reconnect, re-snapshot and diff; never trust the buffer survived
And measure: events received, events coalesced, renders triggered, time-to-consistent after reconnect. Without those numbers, "it feels fast" is all you have.
Real-time is a data guarantee. Interfaces that respect it — by choosing when to paint — end up feeling both live and calm.
