Skip to content

perf(desktop): virtualize message timeline and memoize Markdown for instant channel switching#146

Merged
wesbillman merged 6 commits intomainfrom
perf/channel-switch-virtualization
Mar 21, 2026
Merged

perf(desktop): virtualize message timeline and memoize Markdown for instant channel switching#146
wesbillman merged 6 commits intomainfrom
perf/channel-switch-virtualization

Conversation

@wesbillman
Copy link
Collaborator

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 measureElement handles variable-height content (images, code blocks, etc.).

2. Memoized Markdown component

  • Wrapped Markdown in React.memo with a custom comparator (content, className, compact, tight, mentionNames)
  • Memoized createMarkdownComponents with useMemo — previously created new component function refs on every render, defeating React reconciliation for every message

3. Adapted scroll manager for virtualization

  • scrollToBottom uses virtualizer.scrollToIndex() for reliable bottom-pinning
  • Target message highlighting uses scrollToIndex + requestAnimationFrame to bring off-screen rows into the DOM before querying
  • Extracted settleOnTarget() helper to deduplicate target-scroll state logic
  • Refactored to accept scrollContainerRef from parent, eliminating the merged-ref callback hack

Files changed

  • desktop/package.json — added @tanstack/react-virtual
  • desktop/src/features/messages/ui/MessageTimeline.tsx — virtualizer integration
  • desktop/src/features/messages/ui/useTimelineScrollManager.ts — virtualizer-aware scrolling
  • desktop/src/shared/ui/markdown.tsxReact.memo + useMemo

Testing

  • ✅ TypeScript compiles clean
  • ✅ Biome lint passes (only pre-existing warnings)
  • ✅ File size checks pass
  • ✅ Full CI: rust-fmt, clippy, unit tests, desktop build, Tauri check — all pass

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.

…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
@wesbillman wesbillman merged commit 537e54e into main Mar 21, 2026
8 checks passed
@wesbillman wesbillman deleted the perf/channel-switch-virtualization branch March 21, 2026 16:43
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