perf(desktop): virtualize message timeline and memoize Markdown for instant channel switching#146
Merged
wesbillman merged 6 commits intomainfrom Mar 21, 2026
Merged
Conversation
…nstant channel switching
…croll for virtualized rows - Rename scrollElementRef → scrollContainerRef and setScrollRef → mergedScrollRef for clarity - Bump overscan from 5 → 10 for smoother scrolling experience - Remove redundant measureElement config; ref-based measurement on each row suffices - Use message.id as virtual row key for stable identity across re-renders - Move inline positioning styles to Tailwind classes (relative, absolute, etc.) - Move constants above the type block for conventional ordering - Handle target-message highlighting with virtualization: use scrollToIndex to bring off-screen rows into the DOM before querySelector + scrollIntoView - Add fallback path for non-virtualized usage or unknown target IDs - Add messages to the target-scroll effect dependency array (findIndex needs it)
…e target-scroll logic - Accept scrollContainerRef from parent instead of creating internally, eliminating the merged-ref callback hack in MessageTimeline - Extract settleOnTarget() helper to deduplicate state-setting between virtualizer and fallback target-scroll paths - Fix import ordering (external before internal) - Add biome-ignore comments for stable ref parameter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Channel switching becomes progressively laggier as channels accumulate messages. Despite data-layer caching (#133) and compositor memoization (#143), the core rendering bottleneck remained: all 200+ messages mount to the DOM simultaneously on every channel switch, each with a full ReactMarkdown parser that re-creates component functions on every render.
With agents running 24/7 and filling channels, this degradation was accelerating.
Solution
Three targeted changes that reduce initial channel-switch render work by ~10x:
1. Virtualized message timeline (
@tanstack/react-virtual)Only ~20 messages render at a time (viewport + overscan buffer of 10) instead of the full 200+. Dynamic row measurement via
measureElementhandles variable-height content (images, code blocks, etc.).2. Memoized Markdown component
MarkdowninReact.memowith a custom comparator (content, className, compact, tight, mentionNames)createMarkdownComponentswithuseMemo— previously created new component function refs on every render, defeating React reconciliation for every message3. Adapted scroll manager for virtualization
scrollToBottomusesvirtualizer.scrollToIndex()for reliable bottom-pinningscrollToIndex+requestAnimationFrameto bring off-screen rows into the DOM before queryingsettleOnTarget()helper to deduplicate target-scroll state logicscrollContainerReffrom parent, eliminating the merged-ref callback hackFiles changed
desktop/package.json— added@tanstack/react-virtualdesktop/src/features/messages/ui/MessageTimeline.tsx— virtualizer integrationdesktop/src/features/messages/ui/useTimelineScrollManager.ts— virtualizer-aware scrollingdesktop/src/shared/ui/markdown.tsx—React.memo+useMemoTesting
Note for E2E tests
Virtualization means
[data-message-id]selectors only find rows currently in the DOM. With overscan of 10, most test scenarios with < 20 messages will have all rows rendered. Test authors working with larger datasets should be aware.