Skip to content

perf(site): memoize chat rendering hot path#23720

Merged
kylecarbs merged 2 commits intomainfrom
perf/chat-rendering-memoization
Mar 27, 2026
Merged

perf(site): memoize chat rendering hot path#23720
kylecarbs merged 2 commits intomainfrom
perf/chat-rendering-memoization

Conversation

@kylecarbs
Copy link
Copy Markdown
Member

Addresses chat page rendering performance. Profiling with React Profiler
showed AgentChat actual render times of 20–31ms (exceeding the 16ms/60fps
budget), with StickyUserMessage as the #1 component bottleneck at 35.7%
of self time.

Changes

Hoist createComponents to module scope (response.tsx):
Previously every <Response> instance called createComponents() per
render, creating a fresh components map that forced Streamdown to discard
its cached render tree. Now both light/dark variants are precomputed once
at module scope.

Wrap StickyUserMessage in memo() (ConversationTimeline.tsx):
Profile-confirmed #1 bottleneck. Each instance carries IntersectionObserver

  • ResizeObserver + scroll handlers; skipping re-render avoids all that
    setup.

Wrap ConversationTimeline in memo() (ConversationTimeline.tsx):
Prevents cascade re-renders from the parent when props haven't changed.

Remove duplicate buildSubagentTitles (ConversationTimeline.tsx
AgentDetailContent.tsx): Was computed in both AgentDetailTimeline and
ConversationTimeline. Now computed once and passed as a prop.

Profiling data & analysis

Profiler Metrics

Metric Value
INP (Interaction to Next Paint) 82ms
Processing duration (event handlers) 52ms
AgentChat actual render 20–31ms (budget: 16ms)
AgentChat base render (no memo) ~100ms

Top Bottleneck Components (self-time %)

Component Self Time %
StickyUserMessage 11.0ms 35.7%
ForwardRef (radix-ui) 7.4ms 24.0%
Presence (radix-ui) 2.0ms 6.5%
AgentChatInput 1.4ms 4.5%

Decision log

  • Chose module-scope precomputation over useMemo for createComponents
    because there are only two possible theme variants and they're static.
  • Did not add virtualization — sticky user messages + scroll anchoring
    make it complex. The memoization fixes should be measured first.
  • Did not wrap BlockList in memo() — the React Compiler (enabled for
    pages/AgentsPage/) already auto-memoizes JSX elements inside it.
  • Phase 2 (verify React Compiler effectiveness on parseMessagesWithMergedTools)
    and Phase 3 (radix-ui Tooltip lazy-mounting) deferred to follow-up PRs.

- Hoist createComponents to module scope in response.tsx so every
  Response instance shares stable component references. Previously,
  each render of each Response created a fresh components map,
  forcing Streamdown to discard its cached render tree.

- Wrap StickyUserMessage in memo(). Profiling showed it as the #1
  render bottleneck at 35.7% of component self time. Each instance
  carries IntersectionObserver + ResizeObserver + scroll handlers.

- Wrap ConversationTimeline in memo() to prevent cascade re-renders
  from the parent when props are stable.

- Remove duplicate buildSubagentTitles call from ConversationTimeline.
  It was already computed in AgentDetailTimeline and is now passed
  as a prop instead of recomputed.
@kylecarbs kylecarbs enabled auto-merge (squash) March 27, 2026 14:28
@kylecarbs kylecarbs merged commit 4ed9094 into main Mar 27, 2026
27 checks passed
@kylecarbs kylecarbs deleted the perf/chat-rendering-memoization branch March 27, 2026 14:28
@github-actions github-actions bot locked and limited conversation to collaborators Mar 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants