Skip to content

perf(desktop): memoize composer and timeline to fix typing lag#143

Merged
wesbillman merged 1 commit intomainfrom
fix/composer-typing-perf
Mar 20, 2026
Merged

perf(desktop): memoize composer and timeline to fix typing lag#143
wesbillman merged 1 commit intomainfrom
fix/composer-typing-perf

Conversation

@wesbillman
Copy link
Collaborator

Summary

Fix typing lag/stutter in the message composer caused by unnecessary React re-renders cascading from AppShell through the entire component tree on every keystroke.

Problem

When typing in the MessageComposer, every keystroke triggered re-renders of:

  • ChannelPane (not memoized)
  • MessageTimeline (not memoized — potentially hundreds of message rows)
  • MessageComposerToolbar (not memoized)
  • All autocomplete components

Additionally, inline arrow functions in AppShell (onSend, onCancelReply, onReply, onToggleReaction, onTargetReached) created new references every render, defeating any potential memoization.

Solution

Component memoization (React.memo)

  • ChannelPane — highest-impact change, prevents timeline re-renders on keystrokes
  • MessageTimeline — belt-and-suspenders with ChannelPane memo
  • MessageComposerToolbar — prevents toolbar re-renders during typing
  • ChannelAutocomplete, MentionAutocomplete, ComposerEmojiPicker — defensive memoization

Callback stabilization

  • New useChannelPaneHandlers hook — extracts handleSend, handleReply, handleCancelReply, handleToggleReaction with stable references using useCallback + ref pattern
  • TanStack Query mutation refs.mutateAsync stashed in refs since mutation objects are new references on every state transition (idle → pending → success)
  • contentRef pattern in MessageComposer — callbacks read from ref instead of closing over content state, removing it from dependency arrays
  • Inline handlers extractedhandleCaptureSelection, handlePaperclipClick, handleTargetReached, effectiveToggleReaction

Minor optimizations

  • lineHeight cached in ref (avoids getComputedStyle on every keystroke)
  • sendDisabled memoized via useMemo

Files Changed (9 files)

  • desktop/src/app/AppShell.tsx — stable handler refs, extracted callbacks
  • desktop/src/app/ChannelPane.tsxReact.memo
  • desktop/src/app/useChannelPaneHandlers.tsNEW stable callback hook
  • desktop/src/features/messages/ui/ChannelAutocomplete.tsxReact.memo
  • desktop/src/features/messages/ui/ComposerEmojiPicker.tsxReact.memo
  • desktop/src/features/messages/ui/MentionAutocomplete.tsxReact.memo
  • desktop/src/features/messages/ui/MessageComposer.tsx — ref-based callbacks, cached lineHeight
  • desktop/src/features/messages/ui/MessageComposerToolbar.tsxReact.memo
  • desktop/src/features/messages/ui/MessageTimeline.tsxReact.memo

Future Work (low priority)

  • Memoize MessageComposer itself + stabilize computed props in ChannelPane
  • Memoize TypingIndicatorRow
  • Handle lineHeightRef invalidation on theme/font-size changes

Testing

  • tsc --noEmit
  • biome check ✅ (only pre-existing warnings)
  • Full CI suite passed on push (rust fmt, clippy, tests, desktop build, Tauri check)

@wesbillman wesbillman merged commit d68d0a6 into main Mar 20, 2026
8 checks passed
@wesbillman wesbillman deleted the fix/composer-typing-perf branch March 20, 2026 23:32
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