Skip to content

Commit 41cbe07

Browse files
OpenSource03claude
andcommitted
feat: replace chat rendering with @tanstack/react-virtual for true list virtualization
Replace the old content-visibility + manual pagination approach with @tanstack/react-virtual's useVirtualizer, eliminating the root causes of severe scroll lag during AI streaming and when opening large chats. The old ChatView rendered all 300+ messages in the DOM (relying on CSS content-visibility: auto to defer off-screen painting) and recomputed tool groups, turn summaries, and continuation IDs on every streaming frame via O(n) useMemo passes. Combined with 15+ scroll-tracking refs, ResizeObserver, rAF-throttled handlers, and heavy per-message ReactMarkdown + Prism SyntaxHighlighter rendering, this created compounding performance issues that made the chat unusable for long conversations. Key architectural changes in ChatView.tsx: - True windowing via useVirtualizer: only ~20 DOM nodes exist at any time regardless of chat length (vs 300+ before). Initial render of a 1000-message chat is now O(1) instead of O(n). - Module-level ChatMessageRow component with React.memo and custom comparator: during streaming, only the single streaming message row re-renders per frame. All other rows skip re-render via referential identity checks (React state updates preserve unchanged message object references). - Module-level pure functions buildRows() and estimateRowHeight() extracted outside the component to prevent inline component re-creation per the rerender-no-inline-components best practice. - RowDescriptor discriminated union model that preprocesses messages into a flat virtualizable array, handling tool groups, turn summaries, tool_result skipping, and processing indicator in a single O(n) pass (js-combine-iterations pattern). - Simplified scroll infrastructure: 3 refs (bottomLockedRef, userScrollIntentRef, lastCountRef) replace the previous 15+ refs. The virtualizer owns scroll position tracking internally; we only manage bottom-lock state and user intent detection. - Native scroll container (div with overflow-y: auto) replaces Radix ScrollArea, giving the virtualizer direct access to the scroll element without needing to query internal Radix viewport elements. - Auto-follow during streaming uses ResizeObserver on the inner content container to detect height growth and scrollToIndex to maintain bottom lock, replacing the complex rAF settle timer system. - Scroll-to-message for search navigation uses virtualizer's scrollToIndex with align: "center" instead of manual element querySelector + scrollIntoView with prepend-history expansion. - Content resize handling (for async mermaid diagrams) calls virtualizer.measure() to re-measure all items, replacing the custom CHAT_CONTENT_RESIZED_EVENT + scheduleSettleToBottom chain. - Structural identity caching preserved from old code: tool groups, turn summaries, and continuation IDs only recompute when message structure changes (new message added, tool result arrives), not during streaming content updates. - Tool group morph animation tracking preserved: knownGroupKeysRef, seenUngroupedToolKeysRef, and animatingGroupKeys logic carried over unchanged to maintain the collapse animation for newly finalized tool groups. - Height estimation uses role-based heuristics (32-600px range) with content-length scaling for assistant messages. The virtualizer corrects these via measureElement after first render. Performance characteristics validated against Vercel React best practices: - rendering-content-visibility: replaced by strictly superior true virtualization (items outside viewport don't exist in DOM at all) - rerender-memo: ChatMessageRow uses memo with field-level comparator - rerender-use-ref-transient-values: all scroll state in refs - rerender-defer-reads: scroll position read in handlers, not state - rerender-derived-state: bottom-lock is derived boolean, not continuous scroll position - rerender-no-inline-components: all components and helpers at module level - js-index-maps / js-set-map-lookups: Map and Set for O(1) lookups - advanced-event-handler-refs: onTopScrollProgressRef pattern - client-passive-event-listeners: React 19 onWheel/onTouchMove are passive by default Other changes: - Removed .message-item CSS rule (content-visibility: auto + contain-intrinsic-size) from index.css since virtualization makes it unnecessary. - Added @tanstack/react-virtual dependency (~7KB gzipped, zero deps). - Added .agents/skills/vercel-react-best-practices skill with 62 rules across 8 categories for ongoing performance guidance. - Added .claude/ project configuration files (skills-lock.json, skill definitions). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3d14a39 commit 41cbe07

73 files changed

Lines changed: 8023 additions & 586 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/vercel-react-best-practices/AGENTS.md

Lines changed: 3254 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# React Best Practices
2+
3+
A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
4+
5+
## Structure
6+
7+
- `rules/` - Individual rule files (one per rule)
8+
- `_sections.md` - Section metadata (titles, impacts, descriptions)
9+
- `_template.md` - Template for creating new rules
10+
- `area-description.md` - Individual rule files
11+
- `src/` - Build scripts and utilities
12+
- `metadata.json` - Document metadata (version, organization, abstract)
13+
- __`AGENTS.md`__ - Compiled output (generated)
14+
- __`test-cases.json`__ - Test cases for LLM evaluation (generated)
15+
16+
## Getting Started
17+
18+
1. Install dependencies:
19+
```bash
20+
pnpm install
21+
```
22+
23+
2. Build AGENTS.md from rules:
24+
```bash
25+
pnpm build
26+
```
27+
28+
3. Validate rule files:
29+
```bash
30+
pnpm validate
31+
```
32+
33+
4. Extract test cases:
34+
```bash
35+
pnpm extract-tests
36+
```
37+
38+
## Creating a New Rule
39+
40+
1. Copy `rules/_template.md` to `rules/area-description.md`
41+
2. Choose the appropriate area prefix:
42+
- `async-` for Eliminating Waterfalls (Section 1)
43+
- `bundle-` for Bundle Size Optimization (Section 2)
44+
- `server-` for Server-Side Performance (Section 3)
45+
- `client-` for Client-Side Data Fetching (Section 4)
46+
- `rerender-` for Re-render Optimization (Section 5)
47+
- `rendering-` for Rendering Performance (Section 6)
48+
- `js-` for JavaScript Performance (Section 7)
49+
- `advanced-` for Advanced Patterns (Section 8)
50+
3. Fill in the frontmatter and content
51+
4. Ensure you have clear examples with explanations
52+
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
53+
54+
## Rule File Structure
55+
56+
Each rule file should follow this structure:
57+
58+
```markdown
59+
---
60+
title: Rule Title Here
61+
impact: MEDIUM
62+
impactDescription: Optional description
63+
tags: tag1, tag2, tag3
64+
---
65+
66+
## Rule Title Here
67+
68+
Brief explanation of the rule and why it matters.
69+
70+
**Incorrect (description of what's wrong):**
71+
72+
```typescript
73+
// Bad code example
74+
```
75+
76+
**Correct (description of what's right):**
77+
78+
```typescript
79+
// Good code example
80+
```
81+
82+
Optional explanatory text after examples.
83+
84+
Reference: [Link](https://example.com)
85+
86+
## File Naming Convention
87+
88+
- Files starting with `_` are special (excluded from build)
89+
- Rule files: `area-description.md` (e.g., `async-parallel.md`)
90+
- Section is automatically inferred from filename prefix
91+
- Rules are sorted alphabetically by title within each section
92+
- IDs (e.g., 1.1, 1.2) are auto-generated during build
93+
94+
## Impact Levels
95+
96+
- `CRITICAL` - Highest priority, major performance gains
97+
- `HIGH` - Significant performance improvements
98+
- `MEDIUM-HIGH` - Moderate-high gains
99+
- `MEDIUM` - Moderate performance improvements
100+
- `LOW-MEDIUM` - Low-medium gains
101+
- `LOW` - Incremental improvements
102+
103+
## Scripts
104+
105+
- `pnpm build` - Compile rules into AGENTS.md
106+
- `pnpm validate` - Validate all rule files
107+
- `pnpm extract-tests` - Extract test cases for LLM evaluation
108+
- `pnpm dev` - Build and validate
109+
110+
## Contributing
111+
112+
When adding or modifying rules:
113+
114+
1. Use the correct filename prefix for your section
115+
2. Follow the `_template.md` structure
116+
3. Include clear bad/good examples with explanations
117+
4. Add appropriate tags
118+
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
119+
6. Rules are automatically sorted by title - no need to manage numbers!
120+
121+
## Acknowledgments
122+
123+
Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
name: vercel-react-best-practices
3+
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
4+
license: MIT
5+
metadata:
6+
author: vercel
7+
version: "1.0.0"
8+
---
9+
10+
# Vercel React Best Practices
11+
12+
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 62 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
13+
14+
## When to Apply
15+
16+
Reference these guidelines when:
17+
- Writing new React components or Next.js pages
18+
- Implementing data fetching (client or server-side)
19+
- Reviewing code for performance issues
20+
- Refactoring existing React/Next.js code
21+
- Optimizing bundle size or load times
22+
23+
## Rule Categories by Priority
24+
25+
| Priority | Category | Impact | Prefix |
26+
|----------|----------|--------|--------|
27+
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
28+
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
29+
| 3 | Server-Side Performance | HIGH | `server-` |
30+
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
31+
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
32+
| 6 | Rendering Performance | MEDIUM | `rendering-` |
33+
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
34+
| 8 | Advanced Patterns | LOW | `advanced-` |
35+
36+
## Quick Reference
37+
38+
### 1. Eliminating Waterfalls (CRITICAL)
39+
40+
- `async-defer-await` - Move await into branches where actually used
41+
- `async-parallel` - Use Promise.all() for independent operations
42+
- `async-dependencies` - Use better-all for partial dependencies
43+
- `async-api-routes` - Start promises early, await late in API routes
44+
- `async-suspense-boundaries` - Use Suspense to stream content
45+
46+
### 2. Bundle Size Optimization (CRITICAL)
47+
48+
- `bundle-barrel-imports` - Import directly, avoid barrel files
49+
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
50+
- `bundle-defer-third-party` - Load analytics/logging after hydration
51+
- `bundle-conditional` - Load modules only when feature is activated
52+
- `bundle-preload` - Preload on hover/focus for perceived speed
53+
54+
### 3. Server-Side Performance (HIGH)
55+
56+
- `server-auth-actions` - Authenticate server actions like API routes
57+
- `server-cache-react` - Use React.cache() for per-request deduplication
58+
- `server-cache-lru` - Use LRU cache for cross-request caching
59+
- `server-dedup-props` - Avoid duplicate serialization in RSC props
60+
- `server-hoist-static-io` - Hoist static I/O (fonts, logos) to module level
61+
- `server-serialization` - Minimize data passed to client components
62+
- `server-parallel-fetching` - Restructure components to parallelize fetches
63+
- `server-after-nonblocking` - Use after() for non-blocking operations
64+
65+
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
66+
67+
- `client-swr-dedup` - Use SWR for automatic request deduplication
68+
- `client-event-listeners` - Deduplicate global event listeners
69+
- `client-passive-event-listeners` - Use passive listeners for scroll
70+
- `client-localstorage-schema` - Version and minimize localStorage data
71+
72+
### 5. Re-render Optimization (MEDIUM)
73+
74+
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
75+
- `rerender-memo` - Extract expensive work into memoized components
76+
- `rerender-memo-with-default-value` - Hoist default non-primitive props
77+
- `rerender-dependencies` - Use primitive dependencies in effects
78+
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
79+
- `rerender-derived-state-no-effect` - Derive state during render, not effects
80+
- `rerender-functional-setstate` - Use functional setState for stable callbacks
81+
- `rerender-lazy-state-init` - Pass function to useState for expensive values
82+
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
83+
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
84+
- `rerender-transitions` - Use startTransition for non-urgent updates
85+
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
86+
- `rerender-no-inline-components` - Don't define components inside components
87+
88+
### 6. Rendering Performance (MEDIUM)
89+
90+
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
91+
- `rendering-content-visibility` - Use content-visibility for long lists
92+
- `rendering-hoist-jsx` - Extract static JSX outside components
93+
- `rendering-svg-precision` - Reduce SVG coordinate precision
94+
- `rendering-hydration-no-flicker` - Use inline script for client-only data
95+
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
96+
- `rendering-activity` - Use Activity component for show/hide
97+
- `rendering-conditional-render` - Use ternary, not && for conditionals
98+
- `rendering-usetransition-loading` - Prefer useTransition for loading state
99+
- `rendering-resource-hints` - Use React DOM resource hints for preloading
100+
- `rendering-script-defer-async` - Use defer or async on script tags
101+
102+
### 7. JavaScript Performance (LOW-MEDIUM)
103+
104+
- `js-batch-dom-css` - Group CSS changes via classes or cssText
105+
- `js-index-maps` - Build Map for repeated lookups
106+
- `js-cache-property-access` - Cache object properties in loops
107+
- `js-cache-function-results` - Cache function results in module-level Map
108+
- `js-cache-storage` - Cache localStorage/sessionStorage reads
109+
- `js-combine-iterations` - Combine multiple filter/map into one loop
110+
- `js-length-check-first` - Check array length before expensive comparison
111+
- `js-early-exit` - Return early from functions
112+
- `js-hoist-regexp` - Hoist RegExp creation outside loops
113+
- `js-min-max-loop` - Use loop for min/max instead of sort
114+
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
115+
- `js-tosorted-immutable` - Use toSorted() for immutability
116+
- `js-flatmap-filter` - Use flatMap to map and filter in one pass
117+
118+
### 8. Advanced Patterns (LOW)
119+
120+
- `advanced-event-handler-refs` - Store event handlers in refs
121+
- `advanced-init-once` - Initialize app once per app load
122+
- `advanced-use-latest` - useLatest for stable callback refs
123+
124+
## How to Use
125+
126+
Read individual rule files for detailed explanations and code examples:
127+
128+
```
129+
rules/async-parallel.md
130+
rules/bundle-barrel-imports.md
131+
```
132+
133+
Each rule file contains:
134+
- Brief explanation of why it matters
135+
- Incorrect code example with explanation
136+
- Correct code example with explanation
137+
- Additional context and references
138+
139+
## Full Compiled Document
140+
141+
For the complete guide with all rules expanded: `AGENTS.md`
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Sections
2+
3+
This file defines all sections, their ordering, impact levels, and descriptions.
4+
The section ID (in parentheses) is the filename prefix used to group rules.
5+
6+
---
7+
8+
## 1. Eliminating Waterfalls (async)
9+
10+
**Impact:** CRITICAL
11+
**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
12+
13+
## 2. Bundle Size Optimization (bundle)
14+
15+
**Impact:** CRITICAL
16+
**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
17+
18+
## 3. Server-Side Performance (server)
19+
20+
**Impact:** HIGH
21+
**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
22+
23+
## 4. Client-Side Data Fetching (client)
24+
25+
**Impact:** MEDIUM-HIGH
26+
**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
27+
28+
## 5. Re-render Optimization (rerender)
29+
30+
**Impact:** MEDIUM
31+
**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
32+
33+
## 6. Rendering Performance (rendering)
34+
35+
**Impact:** MEDIUM
36+
**Description:** Optimizing the rendering process reduces the work the browser needs to do.
37+
38+
## 7. JavaScript Performance (js)
39+
40+
**Impact:** LOW-MEDIUM
41+
**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
42+
43+
## 8. Advanced Patterns (advanced)
44+
45+
**Impact:** LOW
46+
**Description:** Advanced patterns for specific cases that require careful implementation.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
title: Rule Title Here
3+
impact: MEDIUM
4+
impactDescription: Optional description of impact (e.g., "20-50% improvement")
5+
tags: tag1, tag2
6+
---
7+
8+
## Rule Title Here
9+
10+
**Impact: MEDIUM (optional impact description)**
11+
12+
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
13+
14+
**Incorrect (description of what's wrong):**
15+
16+
```typescript
17+
// Bad code example here
18+
const bad = example()
19+
```
20+
21+
**Correct (description of what's right):**
22+
23+
```typescript
24+
// Good code example here
25+
const good = example()
26+
```
27+
28+
Reference: [Link to documentation or resource](https://example.com)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: Store Event Handlers in Refs
3+
impact: LOW
4+
impactDescription: stable subscriptions
5+
tags: advanced, hooks, refs, event-handlers, optimization
6+
---
7+
8+
## Store Event Handlers in Refs
9+
10+
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
11+
12+
**Incorrect (re-subscribes on every render):**
13+
14+
```tsx
15+
function useWindowEvent(event: string, handler: (e) => void) {
16+
useEffect(() => {
17+
window.addEventListener(event, handler)
18+
return () => window.removeEventListener(event, handler)
19+
}, [event, handler])
20+
}
21+
```
22+
23+
**Correct (stable subscription):**
24+
25+
```tsx
26+
function useWindowEvent(event: string, handler: (e) => void) {
27+
const handlerRef = useRef(handler)
28+
useEffect(() => {
29+
handlerRef.current = handler
30+
}, [handler])
31+
32+
useEffect(() => {
33+
const listener = (e) => handlerRef.current(e)
34+
window.addEventListener(event, listener)
35+
return () => window.removeEventListener(event, listener)
36+
}, [event])
37+
}
38+
```
39+
40+
**Alternative: use `useEffectEvent` if you're on latest React:**
41+
42+
```tsx
43+
import { useEffectEvent } from 'react'
44+
45+
function useWindowEvent(event: string, handler: (e) => void) {
46+
const onEvent = useEffectEvent(handler)
47+
48+
useEffect(() => {
49+
window.addEventListener(event, onEvent)
50+
return () => window.removeEventListener(event, onEvent)
51+
}, [event])
52+
}
53+
```
54+
55+
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.

0 commit comments

Comments
 (0)