You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: expand CLAUDE.md with performance guidelines for chat rendering
Added detailed performance guidelines to CLAUDE.md, focusing on best practices for building list-heavy and streaming-heavy UIs. Key topics include virtualization techniques, streaming update isolation, the use of refs for transient values, and module-level component definitions. Emphasized the importance of height estimation for virtualizers and provided a reference to performance best practices. This update aims to enhance the overall performance and maintainability of the chat rendering system.
Copy file name to clipboardExpand all lines: CLAUDE.md
+46-1Lines changed: 46 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -353,4 +353,49 @@ The three session IPC handlers share extracted utilities:
353
353
-**Component decomposition** — large components are split into focused sub-components in subdirectories (git/, tool-renderers/, mcp-renderers/, sidebar/)
354
354
-**Hook decomposition** — large hooks are split into focused sub-hooks (session/, useEngineBase)
-**Error tracking** — all caught errors in IPC handlers and hooks must use `reportError(label, err)` (not bare `log()`). Benign/expected catches (cleanup, parse fallbacks, cancellation guards) are exempt. See "Error Tracking (PostHog)" section for details.
356
+
-**Error tracking** — all caught errors in IPC handlers and hooks must use `reportError(label, err)` (not bare `log()`). Benign/expected catches (cleanup, parse fallbacks, cancellation guards) are exempt. See "Error Tracking (PostHog)" section for details.
357
+
358
+
## Performance Guidelines
359
+
360
+
Hard-won lessons from the chat rendering rebuild. Apply these whenever building list-heavy or streaming-heavy UI.
361
+
362
+
### Virtualization over content-visibility
363
+
364
+
**Never use `content-visibility: auto` for long lists.** It keeps all DOM nodes alive (300+ React trees in memory) and merely defers painting. Use `@tanstack/react-virtual` (or equivalent) for true windowing — only ~20 DOM nodes exist regardless of list length. This is the single biggest perf win for large chats.
365
+
366
+
### Streaming update isolation
367
+
368
+
During streaming, only the last message changes. The entire render path must be designed so that only that one component re-renders per frame:
369
+
370
+
-**Referential identity**: React state updates that spread an array (`[...msgs.slice(0, -1), updatedLast]`) preserve object references for unchanged items. `React.memo` with `prev.msg === next.msg` correctly skips them.
371
+
-**Structural identity caching**: expensive derived data (tool groups, turn summaries) should only recompute when the message *structure* changes (new message added, tool result arrives), not when streaming content updates. Cache with a `structureKey` (length + lastId + toolResultCount) and skip recomputation when it hasn't changed.
372
+
-**Never pass the full messages array as a prop to row components** — it changes on every frame. Pass individual message objects or use refs.
373
+
374
+
### Refs for transient values, not state
375
+
376
+
Scroll position, bottom-lock state, animation frame IDs, user scroll intent timestamps — these change on every frame and must **never** be `useState`. Use `useRef` and read them in event handlers. A `useState` for scroll position causes a full re-render on every scroll event.
377
+
378
+
### Module-level components and functions
379
+
380
+
Components defined inside other components (`const Row = () => ...` inside a list component) are re-created on every render, destroying all internal state and remounting the DOM. Always extract to module level. Same for helper functions used in `useMemo` — define them outside the component to avoid stale closure issues and enable referential stability.
381
+
382
+
### Height estimation for virtualizers
383
+
384
+
`@tanstack/react-virtual` needs `estimateSize` for items before measurement. Provide role-based estimates (system: 32px, tool_call: 44px, user: 48-200px, assistant: 40-600px scaled by content length). The virtualizer corrects via `measureElement` after first render. Poor estimates cause scroll jumps but are self-healing.
385
+
386
+
### Explicit height vs CSS padding with border-box
387
+
388
+
When setting explicit `height` on a container, **do not use CSS padding** (`pt-*`, `pb-*`). With Tailwind's `box-sizing: border-box`, padding is subtracted from the content area, shrinking it below what the virtualizer expects. Instead, add padding values directly to the height calculation:
See `.agents/skills/vercel-react-best-practices/` for 62 rules across 8 categories (waterfalls, bundle size, re-renders, rendering, JS perf). Key rules applied in this codebase:
396
+
-`rerender-use-ref-transient-values` — refs for scroll/animation state
0 commit comments