perf(frontend): stop re-rendering whole agent chat on every streamed token#4902
Conversation
…token Each streamed token produces a new render, and an inline `onRewind` closure per row (`() => handleRewind(message)`) defeated memo(AgentMessage) — so all N messages re-rendered per token even though the SDK already keeps settled messages' identity stable (ChatState.replaceMessage clones only the streaming message; sendMessage/regenerate/addToolApprovalResponse are stable instance fields). The only thing forcing the full-list re-render was prop-identity churn. - Stabilize onRewind so memo(AgentMessage) holds: handleRewind is a useCallback reading messages/busy via refs (both change every token; capturing them would recreate the closure and re-defeat the memo), synced in an effect. The parent passes the handler directly; AgentMessage's onRewind now takes the message instead of closing over it per row. This is the load-bearing change — now only the streaming row re-renders, settled rows skip via memo. - memo() the shared Markdown renderer. Narrower win: within the streaming row (the only one that re-renders), its already-settled parts — a reasoning block, text before a tool call — keep the same content string, so this skips re-parsing + re-running Prism on them each token. Defense-in-depth, not the primary fix. Leaves the remaining O(N)-per-token element churn (parent re-render + map) to a message-list virtualization follow-up.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe ChangesAgentChat render stability
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Problem
In the agent chat slice (
AgentChatSlice), every streamed token produces a new render of the conversation, and an inlineonRewindclosure per row —onRewind={() => handleRewind(message)}— gave everyAgentMessagea fresh prop identity each render. That defeatedmemo(AgentMessage), so all N messages re-rendered on every token (and on every keystroke in the composer), each one re-parsing Markdown + re-running Prism highlighting. Long conversations got progressively sluggish.This is purely prop-identity churn: the SDK already hands us stable data. Verified in
@ai-sdk/react3.0-beta /ai6.0-beta:ChatState.replaceMessagerebuilds the array butstructuredClones only the streaming message — settled messages keep their object identity across tokens.sendMessage/regenerate/addToolApprovalResponseare instance arrow-fields created once;setMessagesis auseCallback. All stable across renders.Change
onRewindsomemo(AgentMessage)holds:handleRewindis auseCallbackreadingmessages/busyvia refs (synced in an effect, since both change every token), and the parent passes it directly.AgentMessage'sonRewindnow receives the message instead of closing over it per row. This is the load-bearing change — now only the streaming row re-renders; settled rows skip via memo.memo()the sharedMarkdownrenderer. Narrower, additive win: within the streaming row (the only one that re-renders), its already-settled parts — a reasoning block, text before a tool call — keep the samecontentstring, so they skip re-parse/re-Prism each token. Defense-in-depth, not the primary fix.Before / after
Per streamed token (conversation of N messages):
Not in scope
The parent still re-renders per token and
messages.mapstill allocates N elements — O(N) cheap work. For very long chats that remaining ceiling is addressed by message-list virtualization, left as a follow-up.Testing
tsc --noEmit+ eslint clean on touched files.