A teammate ships a ChatGPT-style chat over the weekend. A flex column of <div> elements for each message; an <input> + <button> at the bottom; a websocket that pipes each token into setMessages(m => updateLast(m, token)). The demo on 10 messages runs silky. By Friday QA loads the real fixture — 1,000 prior messages. The chat panel freezes. Input lag passes 200ms. Every assistant token arrival adds tens of milliseconds of blocking work. The FPS counter idles at 8.

Count the work carefully. One token arrives — setMessages runs — React reconciles 1,001 children — layout walks the 1,001 DOM rows — paint. Per token, every row in the DOM gets touched. At 30 tokens/second over 1,000 rows, the browser is doing 30,000 reconciliation passes per second plus 30 full layout passes over a DOM subtree that takes 25ms to measure on its own. Stable keys help reconciliation, but they don't stop layout from fanning out over children every time the parent re-renders.

ticker tape · every headline live
· 1,000 rows · 30 tok/s · reconciling all · 1,000 rows · 30 tok/s · reconciling all ·
tickercomposerscrolleranchorsupervisordispatcher
news director's note

stage wall · every row reading, every token re-broadcast. one station is all we have — and it's on fire.

FIG. 1 — APPEND-WALL BENCH
prior messages
streaming
rows in DOM10
cost per sec22.5ms
effective FPS60

streaming OFF collapses cost (one render). streaming ON multiplies it by 30 (one render per token). both factors count.

— Slide the prior-message count; toggle streaming. Watch cost. —

At 1,000 prior messages and 30 tokens/sec streaming, the main thread pegs. Which SINGLE number best explains why?

FIG. 2 — THE NAIVE CHAT
1
// The naive chat every junior ships.
2
// Renders ok at 50 messages; pegs the main thread at 1,000 × 30 tokens/sec.
3
function NaiveChat({ messages, onSend }: Props) {
4
  const [input, setInput] = useState('')
5
6
  return (
7
    <div className="flex flex-col h-screen">
8
      <div className="flex-1 overflow-y-auto">
9
        {messages.map((m) => (
10
          <div key={m.id} className="p-3">{m.content}</div>
11
        ))}
12
      </div>
13
      <div className="border-t p-2 flex gap-2">
14
        <input value={input} onChange={(e) => setInput(e.target.value)} />
15
        <button onClick={() => { onSend(input); setInput('') }}>Send</button>
16
      </div>
17
    </div>
18
  )
19
}
20
21
// Streaming hookup:
22
//   onToken((token) => {
23
//     setMessages((m) => updateLast(m, token))   // 30 setStates/sec ✗
24
//   })
25
//
26
// 30 setStates/sec × 1,000 DOM rows =
27
//   30,000 reconciliation passes/sec =
28
//   60ms+ of JS work per second =
29
//   INPUT LAG. CURSOR STUTTER. THIS SHIPS.
30
— Ships fine at 50; pegs the main thread at 1,000. —