Skip to content

chat-ui: memoize timeline rows to stop full re-render per streamed token - #304

Merged
TheGreatAxios merged 1 commit into
mainfrom
cl-6625-chat-latency
Aug 22, 2026
Merged

chat-ui: memoize timeline rows to stop full re-render per streamed token#304
TheGreatAxios merged 1 commit into
mainfrom
cl-6625-chat-latency

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Summary

CL-6625 (chat feels slow/jerky) found the top offender: WorkbenchTimeline (packages/chat-ui/src/timeline.tsx) has no React.memo anywhere, and the items array it receives is rebuilt fresh on every render (mergeStreamingReply/mergePendingSends/appendReplyTimedOutNotice, chat-workspace.tsx:1276-1285). A real reply fires inference.text.delta many times per second (see streaming-reply.ts's own docs), and each one re-renders every MessageParts row in the whole conversation — not just the growing streaming bubble.

Unchanged items keep their own object reference across that rebuild (the merge helpers spread [...items, newItem] without touching existing elements), so a memo comparator that treats handler props as always-equal (they're recreated every render regardless and carry no displayed data, only callbacks) makes memoization actually effective: rows whose data hasn't changed skip re-render entirely during a live turn.

This is the top-ranked fix from the CL-6625 findings comment — smallest safe change, no transport/streaming-architecture rework, and stays out of failed-turn-strip/the composer retry path (CL-6624's territory).

Test plan

  • tsc -p packages/chat-ui/tsconfig.json --noEmit
  • eslint packages/chat-ui/src/timeline.tsx
  • bun run test in packages/chat-ui (665 pass)

…r token

Every streamed inference.text.delta rebuilds the timeline's items array
(mergeStreamingReply in chat-workspace.tsx), which re-rendered every
MessageParts row in the conversation on every token — not just the
growing bubble. Unchanged items keep the same object reference across
that rebuild, so a memo guard that ignores handler props (recreated
every render regardless, carry no displayed data) skips the re-render
for every row whose data didn't actually change.

CL-6625
@TheGreatAxios
TheGreatAxios merged commit d1b3f7b into main Aug 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant