Real-Time Visualization at Scale
How we streamed live player positions over WebSockets and rendered them with Canvas + D3 without melting the browser on a match-day crowd.
The first live match we shipped, the browser froze for eight seconds in the 83rd minute. Around 300,000 viewers saw a still image. My phone was lit up before the final whistle. We figured out what went wrong that night — a message queue that wasn't flushing — and over the next two years we rebuilt most of the pipeline so it couldn't happen that way again. Here's what I learned about putting live data on real screens under real load.
Decouple the socket from the view
The WebSocket should never touch component state. That was the first rule we landed on, and we never broke it again. Incoming frames go into a ring buffer. The view reads the latest value of that ring at its own pace. If the socket gets chatty, nothing downstream of your render loop cares.
socket.onmessage = (e) => {
ring.push(decode(e.data))
}
function tick() {
const frame = ring.latest()
if (frame) draw(frame)
requestAnimationFrame(tick)
}
tick()The moment you wire setState to the socket, you've handed the pace of your renders to the network. That was the bug from match one. We were doing something like onmessage → dispatch → reducer → React → DOM, and a burst of frames could queue minutes of work inside React's scheduler.
Canvas for players, SVG for chrome
Twenty-two little circles on a pitch is trivial for canvas — we've never seen it struggle, not even on mid-range phones. So the pitch outline, the score, the legend all stay in SVG, because those need to be sharp and work for screen readers. The canvas rides on top for the motion. If the canvas ever fails, the page still renders a legible static version of the match. That's a cheap fallback that's saved us more than once.
Backpressure is the only real feature
This is the one I keep trying to push on every team. When the tab goes to the background, stop drawing. When the network stutters, drop old frames instead of queueing them. When the laptop gets warm, halve the sample rate and don't tell anyone. Users never notice these little adjustments. They do notice a page that's seized up.
Real-time doesn't mean every frame. It means the freshest frame you can afford.
I used to think of this as a frontend problem. It isn't. It's a latency-budget problem, and your bit of the budget is the last mile — the one that's easiest to blow if you try to be thorough instead of fast.