FSD Frontend System Design

Part V β€” The Craft Β· Lesson 10.14

YouTube Live Stream Chat UI

In one lineHundreds of messages a second into a fixed-size window β€” buffer, cap, virtualize, and pin to bottom carefully.

Where you've seen itLive chat during a big match, where the feed is a blur and the browser still stays smooth.

  • Time18 min
  • DiagramBuffer and flush pipeline + the bounded window
  • Chapter10 Β· Low Level Design

Diagram

diagram
BUFFER AND FLUSH β€” the core trick

  WebSocket (2.4)
    β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό   ~300 messages/sec
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  in-memory buffer array  β”‚  push only β€” no React state
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ requestAnimationFrame / 100ms tick
               β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  ONE setState per flush  β”‚  ~10 renders/sec instead of 300
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β–Ό
         bounded window

  300 renders/sec = dropped frames. 10 renders/sec = smooth.
diagram
BOUNDED WINDOW β€” memory must not grow with stream length

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  msg 4,981                  β”‚  keep the last N (200–500)
  β”‚  msg 4,982                  β”‚  drop from the head on append
  β”‚  …                          β”‚
  β”‚  msg 5,000  ← newest        β”‚  a 6-hour stream must not
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  hold 2M messages
diagram
PIN TO BOTTOM β€” only when the user is already there

  atBottom = scrollHeight - scrollTop - clientHeight < 40px

  atBottom  β†’ auto-scroll on each flush
  scrolled  β†’ STOP auto-scrolling, show "↓ Jump to latest (128)"
    up
              user is reading history; moving them is hostile

  Also: pause auto-scroll on hover, so a mouse-over freezes the feed.

How it works

  1. Never setState per message. Push into a plain array and flush on a timer or animation frame.
  2. Cap the rendered window to a few hundred messages, dropping the oldest on append.
  3. Virtualize if rows vary in height or the window is large (10.7).
  4. Track atBottom and only auto-scroll when true; otherwise show a jump-to-latest button with an unread count.
  5. Handle the extras that make it real: dedupe by message id, moderation deletions, rate-limit spam per user, and a "chat paused" state under extreme load β€” which is exactly what YouTube does.
js
const buffer = useRef([]);
useEffect(() => {
  socket.onmessage = (e) => buffer.current.push(JSON.parse(e.data));
  const id = setInterval(() => {
    if (!buffer.current.length) return;
    setMessages((prev) => [...prev, ...buffer.current].slice(-MAX));  // cap
    buffer.current = [];
  }, 100);
  return () => clearInterval(id);
}, []);

Trade-offs

DecisionConsequence
Flush every 100ms10 renders/sec, imperceptible delay
Flush per messageSmooth data, unusable UI
Cap at 300 messagesBounded memory, history lost
VirtualizeHandles any size, more complexity
Auto-scroll alwaysDestroys the reading experience

Accessibility (8.3): a live region announcing every message is unusable. Announce nothing by default, offer a "pause and read" mode, and keep the scroll container keyboard-focusable so it can be scrolled without a mouse.

Interview angle

"Design live chat for a stream with 500k viewers."

  • Client side: buffer and flush on a frame, cap the window, virtualize, and pin to bottom only when the user is already there.
  • Transport: SSE or WebSocket per viewer, fanned out from a pub/sub layer β€” and at extreme volume the server samples messages rather than sending all of them.
  • Say the quiet part: nobody can read 300 messages a second, so sampling and "chat paused" are features, not degradations.

Recap

  • Buffer in a ref, flush on a tick β€” one render per frame, not per message.
  • Bound the window; memory must not scale with stream duration.
  • Auto-scroll is conditional on the user already being at the bottom.