Skip to content

perf(site/AgentsSidebar): memoize chat tree derivation#23694

Draft
mafredri wants to merge 1 commit intomainfrom
mf/perf-sidebar-derivation-memoization
Draft

perf(site/AgentsSidebar): memoize chat tree derivation#23694
mafredri wants to merge 1 commit intomainfrom
mf/perf-sidebar-derivation-memoization

Conversation

@mafredri
Copy link
Copy Markdown
Member

@mafredri mafredri commented Mar 26, 2026

Summary

The React Compiler left buildChatTree, new Map, and collectVisibleChatIDs unguarded in AgentsSidebar (94 cache slots). This made every derived value a new object on every render, forcing every ChatTreeNode context consumer to re-render regardless of whether chats changed.

Root cause

When AgentsSidebar compiles to 94 cache slots, the compiler chooses not to guard certain computation chains. Minimal reproductions with the same hook/computation structure ARE guarded; the full component's complexity exceeds the compiler's optimization budget. The result: chatTree, chatById, visibleChatIDs are new references on every render, the ChatTreeContext value is always new, and N ChatTreeNode instances re-render on every sidebar render (navigation, status update, etc.).

Fix

Extract the derivation chain into useDerivedChatTree(chats) with useMemo. The compiler now:

  • Compiles the hook separately (_c(12)), guarding all derivation on chats
  • Guards the ChatTreeContext value in AgentsSidebar (was unguarded, now 12 deps)
  • Guards the chat list JSX section (was unguarded, now 12 deps)
  • AgentsSidebar grows from 94 to 118 cache slots (more expressions are now memoizable)

Performance results

Chrome DevTools Performance recording, 3 sidebar navigation clicks:

Metric Before (main) After (fix) Change
Scripting 7,601 ms 5,588 ms -26.5%
Rendering 2,655 ms 2,426 ms -8.6%
Painting 619 ms 288 ms -53.5%

Proof

Compiled output: Before: 0 guards on 5 derivation statements. After: all 5 inside if ($[0] !== chats) in the hook. Context value creation, previously unguarded, is now behind a 12-dep guard.

Browser profiling: 26.5% scripting reduction on sidebar navigation interactions.

vitest: useDerivedChatTree reference stability tests. Same chats ref → same return object (toBe). Different chats ref → new data. 35 tests pass (33 existing + 2 new).

🤖 This PR was created with the help of Coder Agents, and will be reviewed by a human. 🏂🏻

The React Compiler left buildChatTree, new Map, and
collectVisibleChatIDs unguarded in AgentsSidebar (94 cache
slots, too complex for the compiler to optimize). This made
chatTree, chatById, and visibleChatIDs new objects every
render, forcing every ChatTreeNode context consumer to
re-render.

Extract the derivation chain into useDerivedChatTree with
useMemo keyed on chats. The compiler now:
- Guards all derivation on chats (12 cache slots in the hook)
- Guards the ChatTreeContext value (was unguarded, now 12 deps)
- Guards the chat list JSX (was unguarded, now 12 deps)

AgentsSidebar grows from 94 to 118 cache slots because the
compiler can now memoize more downstream expressions.
Copy link
Copy Markdown
Contributor

@DanielleMaywood DanielleMaywood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If much prefer this wasn't a custom hook. If we need the useMemo, just do it inline

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