Skip to content

perf(frontend): stop re-rendering whole agent chat on every streamed token#4902

Merged
ardaerzin merged 2 commits into
big-agentsfrom
fe-perf/agent-chat-long-conversation-onbig
Jun 28, 2026
Merged

perf(frontend): stop re-rendering whole agent chat on every streamed token#4902
ardaerzin merged 2 commits into
big-agentsfrom
fe-perf/agent-chat-long-conversation-onbig

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Problem

In the agent chat slice (AgentChatSlice), every streamed token produces a new render of the conversation, and an inline onRewind closure per row — onRewind={() => handleRewind(message)} — gave every AgentMessage a fresh prop identity each render. That defeated memo(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/react 3.0-beta / ai 6.0-beta:

  • ChatState.replaceMessage rebuilds the array but structuredClones only the streaming message — settled messages keep their object identity across tokens.
  • sendMessage / regenerate / addToolApprovalResponse are instance arrow-fields created once; setMessages is a useCallback. All stable across renders.

Change

  • Stabilize onRewind so memo(AgentMessage) holds: handleRewind is a useCallback reading messages/busy via refs (synced in an effect, since both change every token), and the parent passes it directly. AgentMessage's onRewind now 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 shared Markdown renderer. 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 same content string, 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):

  • Before: N heavy re-renders (Markdown + Prism + per-message Jotai sub) — also on every keystroke.
  • After: 1 heavy re-render (the streaming row); settled rows are memo-skipped.

Not in scope

The parent still re-renders per token and messages.map still 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.
  • Manually verified in the agent playground: long conversations and typing while streaming no longer drag.

…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.
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 28, 2026
@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jun 28, 2026 10:59am

Request Review

@dosubot dosubot Bot added Frontend refactoring A code change that neither fixes a bug nor adds a feature labels Jun 28, 2026
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c8120ac4-8ef9-4a48-a362-5e097b1aea9a

📥 Commits

Reviewing files that changed from the base of the PR and between 0e30b5e and e66812b.

📒 Files selected for processing (3)
  • web/oss/src/components/AgentChatSlice/assets/markdown.tsx
  • web/oss/src/components/AgentChatSlice/components/AgentChatConversation.tsx
  • web/oss/src/components/AgentChatSlice/components/AgentMessage.tsx

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved chat rewind behavior so it stays accurate during fast, token-by-token updates.
    • Reduced the chance of stale message state affecting rewinds or assistant regeneration.
    • Made markdown rendering more efficient during streamed updates, helping avoid unnecessary re-processing.

Walkthrough

The Markdown component is wrapped in React.memo to avoid re-parsing during streaming. handleRewind in AgentChatConversation is rewritten as a useCallback backed by messagesRef/busyRef refs to prevent stale closures. The onRewind prop on AgentMessage is updated to accept and pass the UIMessage directly.

Changes

AgentChat render stability

Layer / File(s) Summary
Markdown memoization
web/oss/src/components/AgentChatSlice/assets/markdown.tsx
Adds memo import and changes the default export to memo(Markdown); updates comment to describe streaming optimization.
Stable handleRewind via refs and useCallback
web/oss/src/components/AgentChatSlice/components/AgentChatConversation.tsx
Adds messagesRef/busyRef synced by useEffect; rewrites handleRewind as useCallback reading from refs; passes handleRewind directly as onRewind.
onRewind prop contract
web/oss/src/components/AgentChatSlice/components/AgentMessage.tsx
Changes onRewind type from () => void to (message: UIMessage) => void and calls onRewind(message) at the action site.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main performance fix in the agent chat conversation rendering.
Description check ✅ Passed The description is directly related to the changeset and explains the same rendering-performance improvement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fe-perf/agent-chat-long-conversation-onbig

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ardaerzin ardaerzin merged commit 27228f1 into big-agents Jun 28, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend refactoring A code change that neither fixes a bug nor adds a feature size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant