Skip to content

perf(site): defer off-screen messages and cache message parsing#23721

Draft
DanielleMaywood wants to merge 1 commit intomainfrom
daniellemaywood/agents-page-perf
Draft

perf(site): defer off-screen messages and cache message parsing#23721
DanielleMaywood wants to merge 1 commit intomainfrom
daniellemaywood/agents-page-perf

Conversation

@DanielleMaywood
Copy link
Copy Markdown
Contributor

🤖 This PR was written by Coder Agent on behalf of Danielle Maywood 🤖

Follows up on #23720 with three additional performance optimizations for chat navigation, measured at 43–60% reduction in long-task blocking time via PerformanceObserver.

Navigation Baseline (ms) With fixes (ms) Improvement
→ Debug chat 511 291 43%
→ Convert delete flows 729 295 60%
→ Fix agent chat messages 521 243 53%

Changes

Batch layout reads in StickyUserMessage.update() (ConversationTimeline.tsx): Moves all getBoundingClientRect/offsetHeight reads before any style writes, eliminating forced reflow violations. Previously each read after a write forced the browser to flush pending layout.

Add WeakMap parse cache for parseMessageContent (messageParsing.ts): The store preserves ChatMessage object identity for unchanged messages, so only the actively-changing message gets re-parsed per store tick. Avoids mutating cached objects by spreading a new entry with merged tools. This was explicitly deferred as "Phase 2" in #23720.

Defer off-screen message rendering (ConversationTimeline.tsx): On chat navigation scroll starts at the bottom, so messages above the fold get a lightweight placeholder instead of the full Streamdown pipeline. IntersectionObserver with 600px rootMargin triggers real rendering before the user scrolls to them. Only activates for conversations with 8+ messages. The existing ResizeObserver in ScrollAnchoredContainer handles scroll compensation when placeholders expand.

Profiling data & analysis

React DevTools Profiler (pre-fix baseline)

Top components by self-time from 123-commit profiling session:

Component Total self Renders Max self
Ct (Streamdown internal) 321ms 4 38ms
AgentChatInput 132ms 37 5.3ms
Memo(_c0) 122ms 31 5.4ms
AgentDetailTopBar 86ms 73 3.0ms
Streamdown 62ms 7 12.3ms

Navigation commits (#34, #81, #94) dominated by Ct at 25–182ms self-time — all from synchronous Streamdown markdown parsing during mount. The deferred rendering eliminates this for off-screen messages.

Long Task measurement methodology

Used PerformanceObserver({ entryTypes: ["longtask"] }) via agent-browser to measure blocking time during identical navigation sequences on the same dataset, with and without fixes applied.

Decision log

  • Chose WeakMap over useMemo for parse cache because parseMessagesWithMergedTools is a plain function, not a hook, and the cache benefits all callers including getEditableUserMessagePayload.
  • Chose IntersectionObserver deferral over full virtualization — perf(site): memoize chat rendering hot path #23720 noted sticky messages + scroll anchoring make virtualization complex. DeferredMessage sidesteps this entirely.
  • IMMEDIATE_RENDER_COUNT = 5 and DEFERRED_THRESHOLD = 8 chosen empirically — 5 messages fill the viewport, and conversations under 8 messages have negligible mount cost.

Follows up on #23720 with two additional performance optimizations
for chat navigation, measured at 43-60% reduction in long-task
blocking time via PerformanceObserver.

Batch layout reads in StickyUserMessage.update() — moves all
getBoundingClientRect/offsetHeight reads before any style writes,
eliminating forced reflow violations.

Add WeakMap parse cache for parseMessageContent — the store
preserves ChatMessage object identity for unchanged messages, so
only the actively-changing message gets re-parsed per store tick.
Avoids mutating cached objects by spreading a new entry with
merged tools.

Defer off-screen message rendering — on chat navigation scroll
starts at the bottom, so messages above the fold get a lightweight
placeholder. IntersectionObserver with 600px rootMargin triggers
real rendering before the user scrolls to them. Only activates for
conversations with 8+ messages.
@github-actions github-actions bot added the community Pull Requests and issues created by the community. label Mar 27, 2026
@DanielleMaywood DanielleMaywood removed the community Pull Requests and issues created by the community. label Mar 27, 2026
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.

2 participants