Skip to content

Commit e243248

Browse files
committed
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.
1 parent 41cbe07 commit e243248

1 file changed

Lines changed: 46 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,49 @@ The three session IPC handlers share extracted utilities:
353353
- **Component decomposition** — large components are split into focused sub-components in subdirectories (git/, tool-renderers/, mcp-renderers/, sidebar/)
354354
- **Hook decomposition** — large hooks are split into focused sub-hooks (session/, useEngineBase)
355355
- **Shared components** — reusable UI patterns extracted to shared components (`TabBar`, `PanelHeader`, `SettingRow`)
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.
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:
389+
```tsx
390+
style={{ height: `${virtualizer.getTotalSize() + headerSpace + bottomSpace}px` }}
391+
```
392+
393+
### Performance best practices reference
394+
395+
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
397+
- `rerender-no-inline-components` — module-level components
398+
- `rerender-memo` — custom comparators on row components
399+
- `js-index-maps` / `js-set-map-lookups` — Map/Set for O(1) lookups
400+
- `js-combine-iterations` — single-pass row building
401+
- `advanced-event-handler-refs` — callback refs to avoid effect re-subscription

0 commit comments

Comments
 (0)