Repository navigation
perf(chat): re-render only the streaming reply per chunk - #1191
Merged
Merged
Conversation
The streaming message was useReducer state inside useAgentSession, so every SSE chunk re-ran ChatWindow, the hook and the whole composer (ChatInput, ModelSelector) and the minimap. It now lives in a small StreamingStore. ChatWindow subscribes only to whether a reply streams and has content; a memo'd StreamingReply subscribes to the message and updates the prompt anchor after each commit; ChatMinimap reads the message through a getter when it measures. ChatInput and ChatMinimap are memo'd for the renders a running tool still causes. Subscriptions use a state update, not useSyncExternalStore, so React keeps batching the chunks that arrive before it renders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Grant-Vranes
pushed a commit
to Grant-Vranes/pi-web-Desktop-Electron
that referenced
this pull request
Oct 13, 2026
Resolve conflicts keeping the fork's project-rail sidebar, brand/versions hero, minimap turn anchors and i18n while adopting upstream's streaming render refactor (agegr#1191), session deep links, notice splitting and other v0.12.0 changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The streaming message was
useReducerstate insideuseAgentSession, and that hook runs inChatWindow. Every SSE chunk therefore re-ranChatWindow, the hook, the whole composer (ChatInput,ModelSelector) and the minimap, although only the reply's bubble changes.A props diff in the profiler (old vs new
memoizedPropsper render) showedChatInputwith identical props in 543 of its 555 renders during one reply.ChatMinimapre-rendered only because itsstreamingMessageprop changed, and it reads that prop only inside its 150 ms throttled measure. Together these four components took about 18% of commit time while streaming, once the Markdown fix (#1176) was in.Change
lib/streaming-message.tsaddscreateStreamingStore(): the existingstreamReducer, plussubscribe.useAgentSessiondispatches to it and returnsstreamingStoreinstead ofstreamState.StreamingReply(memo'd, inChatWindow.tsx) subscribes to the message and renders itsMessageView. After each commit it runs the prompt anchor's update, which the olduseLayoutEffectkeyed onstreamState.streamingMessagedid.ChatWindowstill runs that update when the bubble appears or goes.ChatMinimaptakesgetStreamingMessage()instead of the message and reads it when it measures the turns.ChatInputandChatMinimaparememo'd, for the renders a running tool still causes. An AST scan of their render bodies found no reads of outside mutable values, onlyChatInputcopying its own state into refs.Why not
useSyncExternalStorehooks/useStreamingSelector.tssubscribes with a plain state update.useSyncExternalStorerenders every store change on its own, while state updates let React batch the SSE events that arrive before it renders. More renders would mean more parses of the reply's last Markdown block.The store calls its listeners synchronously in
dispatch, somessage_endappending the reply and ending the stream stays one render, as before.Measurements
Production build with profiling, 4x CPU throttle. The same prompt (a long Chinese article, no tools) was sent on
mainand on this branch.ChatWindowrenders / self timeChatInputrendersModelSelectorrendersChatMinimaprendersStreamingReplyrenders / self timeThe two replies differ in length (1732 vs 1990 SSE events), hence the per-event rows.
Checks
npm test: 2625 pass, 0 fail (new test: the store notifies only on a change and stops after unsubscribe).tsc --noEmitandeslint .are clean.node e2e/run.mjs: 27/27 on the first run.mainand this branch side by side (two prompts in one session, sampled every 150 ms during the second):demo/keeps its own copy of these components and is unchanged.🤖 Generated with Claude Code