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.
stage wall · every row reading, every token re-broadcast. one station is all we have — and it's on fire.
streaming OFF collapses cost (one render). streaming ON multiplies it by 30 (one render per token). both factors count.
At 1,000 prior messages and 30 tokens/sec streaming, the main thread pegs. Which SINGLE number best explains why?
// The naive chat every junior ships.// Renders ok at 50 messages; pegs the main thread at 1,000 × 30 tokens/sec.function NaiveChat({ messages, onSend }: Props) { const [input, setInput] = useState('') return ( <div className="flex flex-col h-screen"> <div className="flex-1 overflow-y-auto"> {messages.map((m) => ( <div key={m.id} className="p-3">{m.content}</div> ))} </div> <div className="border-t p-2 flex gap-2"> <input value={input} onChange={(e) => setInput(e.target.value)} /> <button onClick={() => { onSend(input); setInput('') }}>Send</button> </div> </div> )}// Streaming hookup:// onToken((token) => {// setMessages((m) => updateLast(m, token)) // 30 setStates/sec ✗// })//// 30 setStates/sec × 1,000 DOM rows =// 30,000 reconciliation passes/sec =// 60ms+ of JS work per second =// INPUT LAG. CURSOR STUTTER. THIS SHIPS.