Problem
During SSE streaming, the live streaming text is baked into the same array that holds all historical messages, so every animation frame rebuilds the entire message list and re-runs the full filter/dedup/sort/group pipeline (~60×/sec). Cost scales with thread length — visible jank on long sessions.
Evidence
src/screens/chat/chat-screen.tsx:1469 — finalDisplayMessages useMemo builds a NEW array embedding __streamingText: stableActiveStreamingText (line ~1573), with activeRealtimeStreamingText in its deps (~1605). Every rAF tick → new array identity.
src/screens/chat/components/chat-message-list.tsx:2143 — areChatMessageListEqual short-circuits on prev.messages === next.messages and :2160 on prev.streamingText === next.streamingText — both fail every frame → full list re-render.
- Inside the list,
displayMessages (:798, filter+dedup), displayEntries (:882), toolResultsByCallId (:1055), toolInteractionCount (:1125), and the streamingState signature map (:1160) all recompute over the FULL message set per frame.
- Two independent rAF typewriter loops run simultaneously for the same stream:
use-streaming-message.ts:345-373 (pushTargetText rAF) and use-smooth-streaming-text.ts:56-78, fed at chat-screen.tsx:1444 — every revealed character pays the above cost twice.
Per-message memoization (MessageItem memo + areMessagesEqual) is solid and skips correctly — the O(N) work is all at the list level.
Fix
- Render the streaming bubble as an isolated sibling component that subscribes to streaming state directly; keep
finalDisplayMessages free of live text. OR memoize displayEntries/toolResultsByCallId on a content signature excluding __streamingText.
- Drop one of the two rAF smoothing layers (the store text is already smoothed once).
Found in chat-area audit 2026-06-11 (Opus agent, verified against source).
Problem
During SSE streaming, the live streaming text is baked into the same array that holds all historical messages, so every animation frame rebuilds the entire message list and re-runs the full filter/dedup/sort/group pipeline (~60×/sec). Cost scales with thread length — visible jank on long sessions.
Evidence
src/screens/chat/chat-screen.tsx:1469—finalDisplayMessagesuseMemo builds a NEW array embedding__streamingText: stableActiveStreamingText(line ~1573), withactiveRealtimeStreamingTextin its deps (~1605). Every rAF tick → new array identity.src/screens/chat/components/chat-message-list.tsx:2143—areChatMessageListEqualshort-circuits onprev.messages === next.messagesand:2160onprev.streamingText === next.streamingText— both fail every frame → full list re-render.displayMessages(:798, filter+dedup),displayEntries(:882),toolResultsByCallId(:1055),toolInteractionCount(:1125), and the streamingState signature map (:1160) all recompute over the FULL message set per frame.use-streaming-message.ts:345-373(pushTargetTextrAF) anduse-smooth-streaming-text.ts:56-78, fed atchat-screen.tsx:1444— every revealed character pays the above cost twice.Per-message memoization (
MessageItemmemo +areMessagesEqual) is solid and skips correctly — the O(N) work is all at the list level.Fix
finalDisplayMessagesfree of live text. OR memoizedisplayEntries/toolResultsByCallIdon a content signature excluding__streamingText.Found in chat-area audit 2026-06-11 (Opus agent, verified against source).