<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: shrutictc2</title>
    <description>The latest articles on DEV Community by shrutictc2 (@shrutictc2).</description>
    <link>https://dev.to/shrutictc2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024616%2F93cf8bab-a628-41f8-92dc-c9f2ce839cfa.png</url>
      <title>DEV Community: shrutictc2</title>
      <link>https://dev.to/shrutictc2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shrutictc2"/>
    <language>en</language>
    <item>
      <title>How I Kept a Live Chat Feed Smooth at 3,700+ Messages</title>
      <dc:creator>shrutictc2</dc:creator>
      <pubDate>Sat, 11 Jul 2026 00:30:15 +0000</pubDate>
      <link>https://dev.to/shrutictc2/how-i-kept-a-live-chat-feed-smooth-at-3700-messages-1kf6</link>
      <guid>https://dev.to/shrutictc2/how-i-kept-a-live-chat-feed-smooth-at-3700-messages-1kf6</guid>
      <description>&lt;p&gt;I built &lt;a href="https://liveshop-mocha.vercel.app" rel="noopener noreferrer"&gt;LiveShop&lt;/a&gt;, a mini live-shopping stream UI, to answer a question I kept running into as a frontend-curious grad: tutorials teach you how to render a list, but they never teach you what happens when that list gets hit with the kind of traffic a real live stream produces. So I built something that would force the problem to show up, then fixed it, then measured whether the fix actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;LiveShop simulates a live-shopping broadcast - the kind of interface a small merchant might use to sell products while streaming. A mock event engine fires chat messages, reactions, and purchase notifications on an interval, standing in for what a real WebSocket connection to a streaming backend would deliver. On top of that sits a chat feed, a scrollable product carousel, and a floating reaction animation layer.&lt;/p&gt;

&lt;p&gt;None of that is unusual. The interesting part started once I asked: what happens when message volume spikes?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;A naive chat feed is just &lt;code&gt;messages.map(m =&amp;gt; &amp;lt;ChatRow key={m.id} {...m} /&amp;gt;)&lt;/code&gt;. It's the first thing anyone reaches for, and it's fine — right up until it isn't. At 50 messages, nothing looks wrong. At a few hundred, every new message triggers a full re-render pass across every row in the DOM, including the hundreds that have already scrolled out of view and that nobody can see. The browser is doing layout and paint work for pixels that aren't on screen.&lt;/p&gt;

&lt;p&gt;In a real live stream, this is exactly the wrong failure mode, because message volume doesn't arrive evenly. It spikes — right after a product drop, right when something funny happens on stream, right when a popular creator says something quotable. That's precisely the moment a chat feed can't afford to stutter, and precisely the moment a naive implementation is most likely to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;Rather than guess whether this mattered, I built a way to test it directly. LiveShop has a "Simulate spike" button that fires 500 messages instantly, plus a live FPS readout using &lt;code&gt;requestAnimationFrame&lt;/code&gt; to track real frame timing — not a theoretical estimate, an actual rolling average of how long frames were taking on my machine as the spike happened.&lt;/p&gt;

&lt;p&gt;With a plain &lt;code&gt;.map()&lt;/code&gt; render, pushing the feed to roughly 3,700 accumulated messages dropped frame rate to &lt;strong&gt;25fps&lt;/strong&gt;, with visible stutter. A quick console check — &lt;code&gt;document.querySelectorAll('.chat-row').length&lt;/code&gt; — confirmed why: &lt;strong&gt;3,333 DOM nodes&lt;/strong&gt;, one for every message that had ever arrived, all still mounted and being diffed on every update.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;I virtualized the list with &lt;a href="https://github.com/bvaughn/react-window" rel="noopener noreferrer"&gt;&lt;code&gt;react-window&lt;/code&gt;&lt;/a&gt;. Instead of mounting a DOM node per message, it only renders the rows that are actually inside the visible scroll viewport — roughly 12–15 at a time — and recycles them as the user scrolls. The feed can hold 50 messages or 50,000; the DOM footprint stays constant either way.&lt;/p&gt;

&lt;p&gt;After switching over, the same spike test — thousands of messages, fired as fast as the mock stream could generate them — held a steady &lt;strong&gt;60fps&lt;/strong&gt;, and the DOM node count settled at &lt;strong&gt;14&lt;/strong&gt;, regardless of how high the message count climbed.&lt;/p&gt;

&lt;p&gt;The FPS number is the visible symptom. The DOM node count is the actual mechanism, and it's the more interesting one, because it holds true even before performance visibly degrades — it's not something you'd necessarily notice by eye until the spike hits, but it's there from message one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few other load-bearing decisions
&lt;/h2&gt;

&lt;p&gt;Virtualization solved the big one, but a few smaller choices mattered too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reaction animations are capped at 8 concurrent bubbles.&lt;/strong&gt; Reactions can arrive multiple times a second in a busy stream, and animating every single one would spawn DOM nodes faster than the browser could clean them up. Capping the count doesn't lose meaningful reactions — they arrive faster than a human can register each one individually anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat history is capped at 5,000 messages client-side.&lt;/strong&gt; A long-running stream shouldn't grow memory without bound, so old messages page out once the cap is hit — the same pattern a production client would need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mock event stream sits behind a single callback interface.&lt;/strong&gt; &lt;code&gt;startLiveEventStream(onEvent)&lt;/code&gt; is the entire contract. Every component downstream — chat, reactions, viewer count — only depends on that event shape, not on how the events arrive. Swapping the &lt;code&gt;setInterval&lt;/code&gt; mock for a real WebSocket connection later would mean changing exactly one file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd do differently with more time
&lt;/h2&gt;

&lt;p&gt;The FPS readout is honest but manual — I watched the number and wrote down what I saw. A more rigorous version would script the spike test and assert against a performance trace automatically, the way you'd want a real regression test to work rather than something a developer eyeballs once and trusts forever. That's the next thing I'd build if I kept going.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;LiveShop is &lt;a href="https://github.com/shrutictc2/liveshop" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt; — the README has the full before/after numbers and architecture notes if you want to dig deeper.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
