We had a live event feature that we were genuinely proud of. A real-time feed with live commentary, updated scores, and user reactions. Tested fine up to about 2,000 simulated users in our load tests. Then we had our first real event.
At 11:43am, with about 50,000 users on the page simultaneously, we started getting reports of browsers freezing. By 11:47am, CPU usage graphs on our frontend monitoring were showing JavaScript main thread utilisation above 95% for significant portions of our user base. The page wasn't crashing — it was just completely unresponsive.
What Was Happening
The live feed component received WebSocket updates and stored them in a context that was consumed by three different child components — the main feed, a sidebar counter, and a compact notification badge.
The bug was in how we'd structured the context updates. Every incoming WebSocket message (arriving at roughly 200/second during peak activity) triggered a state update on the feed context. Because all three consuming components were subscribed to the same context, all three re-rendered on every update — including the main feed, which was doing a list reconciliation of potentially hundreds of items on each render.
200 messages per second × 3 re-rendering components × list reconciliation on each = the main thread doing nothing but React reconciliation work and dropping all user interaction events.
Why Our Tests Didn't Catch It
At 2,000 users the message rate was about 12/second. At that rate, the re-renders were happening but the browser could keep up. The JavaScript main thread had enough headroom. At 50,000 users producing 200 messages per second, it didn't.
This is a fundamental testing gap: we load tested the server (which handled the scale fine) but not the client-side rendering under high event rate. Client-side performance at scale is a testing category many teams skip because the infrastructure doesn't produce it in test environments.
The Fix
We split the context into three separate contexts: one for the counter (updated on every message), one for the badge (updated on every message), and one for the feed list itself (updated with batched, throttled updates at maximum 10 times per second). Components only subscribed to the context they needed, and the feed list re-renders dropped from 200/second to at most 10/second.
We also added React.memo() to the individual feed item components so the unchanged items in the list didn't re-render when new items were added.
The Takeaway
Context is a blunt instrument for high-frequency updates. When the update rate is high, the architecture of your state and context subscriptions is a performance concern, not just a code organisation concern. Test your client-side performance under realistic update rates before your users discover the problem for you.