
Render Optimization
- 15 installs
- 213 repo stars
- Updated August 4, 2026
- yonatangross/orchestkit
Helps with ai & agent building tasks.
About
render-optimization is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- render-optimization
- AI & Agent Building
- AI-coding skill
Render Optimization by the numbers
- 15 all-time installs (skills.sh)
- Ranked #11,165 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yonatangross/orchestkit --skill render-optimizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 213 |
| Last updated | August 4, 2026 |
| Repository | yonatangross/orchestkit ↗ |
What it does
Helps with ai & agent building tasks.
Files
React Render Optimization
Modern render performance patterns for React 19+.
Decision Tree: React Compiler First (2026)
Is React Compiler enabled?
├─ YES → Let compiler handle memoization automatically
│ Only use useMemo/useCallback as escape hatches
│ DevTools shows "Memo ✨" badge
│
└─ NO → Profile first, then optimize
1. React DevTools Profiler
2. Identify actual bottlenecks
3. Apply targeted optimizationsReact Compiler (Primary Approach)
React 19's compiler automatically memoizes:
- Component re-renders
- Intermediate values (like useMemo)
- Callback references (like useCallback)
- JSX elements
// next.config.js (Next.js 16+)
const nextConfig = {
reactCompiler: true,
}
// Expo SDK 54+ enables by defaultVerification: Open React DevTools → Look for "Memo ✨" badge
When Manual Memoization Still Needed
Use useMemo/useCallback as escape hatches when:
// 1. Effect dependencies that shouldn't trigger re-runs
const stableConfig = useMemo(() => ({
apiUrl: process.env.API_URL
}), [])
useEffect(() => {
initializeSDK(stableConfig)
}, [stableConfig])
// 2. Third-party libraries without compiler support
const memoizedValue = useMemo(() =>
expensiveThirdPartyComputation(data), [data])
// 3. Precise control over memoization boundaries
const handleClick = useCallback(() => {
// Critical callback that must be stable
}, [dependency])Virtualization Thresholds
| Item Count | Recommendation |
|---|---|
| < 100 | Regular rendering usually fine |
| 100-500 | Consider virtualization |
| 500+ | Virtualization required |
import { useVirtualizer } from '@tanstack/react-virtual'
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
})State Colocation
Move state as close to where it's used as possible:
// ❌ State too high - causes unnecessary re-renders
function App() {
const [filter, setFilter] = useState('')
return (
<Header /> {/* Re-renders on filter change! */}
<FilterInput value={filter} onChange={setFilter} />
<List filter={filter} />
)
}
// ✅ State colocated - minimal re-renders
function App() {
return (
<Header />
<FilterableList /> {/* State inside */}
)
}Profiling Workflow
1. React DevTools Profiler: Record, interact, analyze 2. Identify: Components with high render counts or duration 3. Verify: Is the re-render actually causing perf issues? 4. Fix: Apply targeted optimization 5. Measure: Confirm improvement
Quick Wins
1. Key prop: Stable, unique keys for lists 2. Lazy loading: React.lazy() for code splitting 3. Debounce: Input handlers with useDeferredValue 4. Suspense: Streaming with proper boundaries
Key Decisions
| Decision | Recommendation |
|---|---|
| Memoization | Let React Compiler handle it (2026 default) |
| Lists 100+ items | Use TanStack Virtual |
| State placement | Colocate as close to usage as possible |
| Profiling | Always measure before optimizing |
Related Skills
react-server-components-framework- Server-first renderingvite-advanced- Build optimizatione2e-testing- Performance testing with Playwright
References
- React Compiler Migration - Compiler adoption
- Memoization Escape Hatches - When useMemo needed
- TanStack Virtual - Virtualization
- State Colocation - State placement
- DevTools Profiler - Finding bottlenecks
React Performance Audit Checklist
Pre-deployment performance verification.
React Compiler Check
- [ ] React Compiler enabled in build config
- [ ] Components show "Memo ✨" badge in DevTools
- [ ] Code follows Rules of React:
- [ ] Components are idempotent
- [ ] Props/state treated as immutable
- [ ] Side effects in useEffect only
- [ ] Hooks at top level
Render Performance
- [ ] No unnecessary re-renders (verified with Profiler)
- [ ] State colocated close to usage
- [ ] Context split to prevent cascading updates
- [ ] Expensive computations have escape hatch memoization
- [ ] Lists > 100 items are virtualized
Large Lists / Data
- [ ] TanStack Virtual for lists > 100 items
- [ ] Pagination or infinite scroll for API data
- [ ] Table virtualization for grids > 50 rows
- [ ] Images lazy loaded below fold
Code Splitting
- [ ] Route-based code splitting (lazy routes)
- [ ] Heavy components lazy loaded
- [ ] Dynamic imports for large libraries
- [ ] Bundle analyzer run, no unexpected large chunks
Network Performance
- [ ] API calls deduplicated (React Query, SWR)
- [ ] Data prefetched on hover/intent
- [ ] Optimistic updates for mutations
- [ ] Appropriate cache headers set
Images & Media
- [ ] Images optimized (WebP, AVIF)
- [ ] Responsive images with srcset
- [ ] Lazy loading for below-fold images
- [ ] Placeholder/skeleton during load
Third-Party Scripts
- [ ] Analytics loaded async/deferred
- [ ] Third-party widgets lazy loaded
- [ ] Font loading optimized (preload critical)
- [ ] No render-blocking resources
Profiling Verification
Before Optimization
1. [ ] Record baseline interaction times 2. [ ] Document slowest components 3. [ ] Note current bundle size
After Optimization
1. [ ] Re-profile all interactions 2. [ ] Verify improvements in numbers 3. [ ] Check bundle size delta
Key Metrics to Track
| Metric | Target | Current |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | ___ |
| FID (First Input Delay) | < 100ms | ___ |
| CLS (Cumulative Layout Shift) | < 0.1 | ___ |
| Time to Interactive | < 3s | ___ |
| Main thread blocking | < 200ms | ___ |
Quick Profiler Commands
# React DevTools Profiler
# 1. Open DevTools → Profiler tab
# 2. Click Record
# 3. Perform interaction
# 4. Click Stop
# 5. Analyze flamegraph
# Lighthouse
npx lighthouse http://localhost:3000 --view
# Bundle Analyzer (Next.js)
ANALYZE=true npm run build
# Bundle Analyzer (Vite)
npx vite-bundle-visualizerCommon Issues Checklist
- [ ] No anonymous functions as props in hot paths
- [ ] No object/array literals as props in hot paths
- [ ] Context providers near consumers
- [ ] useEffect dependencies correct
- [ ] No state updates in render
Sign-Off
- [ ] All critical interactions < 100ms
- [ ] No visible jank during scroll
- [ ] Page load acceptable on 3G
- [ ] Bundle size within budget
- [ ] Performance regression tests in CI
React DevTools Profiler Workflow
Finding and fixing performance bottlenecks.
Setup
1. Install React DevTools browser extension 2. Open DevTools (F12) 3. Navigate to Profiler tab 4. Ensure React is in development mode
Basic Profiling Flow
1. Start Recording
- Click the blue Record button
- Perform the slow interaction
- Click Stop
2. Analyze the Flamegraph
The flamegraph shows component render times:
[App (2ms)]
├── [Header (0.5ms)]
├── [Sidebar (15ms)] ← Slow!
│ ├── [NavItem (1ms)]
│ ├── [NavItem (1ms)]
│ └── [HeavyWidget (12ms)] ← Found it!
└── [Content (1ms)]3. Key Metrics
| Metric | Meaning |
|---|---|
| Render time | How long component took to render |
| Commit time | Time to apply changes to DOM |
| Interactions | What triggered the render |
Reading the Profiler
Color Coding
- Gray: Did not render
- Blue/Teal: Rendered (fast)
- Yellow: Rendered (medium)
- Red/Orange: Rendered (slow)
"Why did this render?"
Enable in DevTools settings: 1. Click gear icon in Profiler 2. Check "Record why each component rendered"
Common reasons:
- Props changed
- State changed
- Parent rendered
- Context changed
- Hooks changed
Identifying Problems
Problem 1: Component Renders Too Often
Look for components that render on every interaction:
Render 1: [List (50ms)] - items changed ✓
Render 2: [List (50ms)] - items same, parent rendered ✗
Render 3: [List (50ms)] - items same, parent rendered ✗Solution: Isolate state, use React.memo as escape hatch
Problem 2: Single Render Too Slow
Look for wide bars in the flamegraph:
[SlowComponent (200ms)]
├── [Child1 (5ms)]
├── [Child2 (190ms)] ← Find the slow child
│ └── [GrandChild (185ms)] ← Root cause
└── [Child3 (5ms)]Solution: Virtualize, lazy load, or optimize computation
Problem 3: Cascading Re-renders
Many components re-render for one change:
[Parent] → [Child1] → [GrandChild1]
→ [Child2] → [GrandChild2]
→ [Child3] → [GrandChild3]Solution: Move state down, split context
Profiler Settings
Click the gear icon for options:
- Record why each component rendered: Essential for debugging
- Hide commits below X ms: Filter noise
- Highlight updates: Visual indicator during interaction
Ranked View
Switch from Flamegraph to Ranked view:
1. HeavyWidget 12ms
2. Sidebar 3ms
3. NavItem 1ms
4. Content 1ms
5. Header 0.5msThis shows components sorted by render time.
Timeline View
Shows renders over time, useful for:
- Finding render cascades
- Identifying what triggered re-renders
- Seeing interaction-to-render timing
Console Integration
// Add profiling in code
import { Profiler } from 'react'
function onRenderCallback(
id, // Component tree id
phase, // "mount" | "update"
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`)
}
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>Quick Checklist
1. [ ] Record the slow interaction 2. [ ] Find the slowest component (ranked view) 3. [ ] Check why it rendered (DevTools setting) 4. [ ] Verify if render was necessary 5. [ ] Apply targeted fix 6. [ ] Re-profile to confirm improvement
Common Fixes by Cause
| Why Rendered | Fix |
|---|---|
| Props changed (but same value) | Check prop references |
| Parent rendered | Isolate state, split component |
| Context changed | Split context |
| Hooks changed | Check effect dependencies |
| State changed | Verify state is necessary |
Memoization Escape Hatches
When to still use useMemo and useCallback with React Compiler.
Overview
React Compiler handles most memoization automatically. Use manual memoization only as escape hatches for specific cases.
Escape Hatch 1: Effect Dependencies
When a value is used as an effect dependency and you need precise control:
// Problem: Effect runs on every render
function UserDashboard({ userId }) {
const config = {
userId,
includeStats: true,
format: 'detailed',
}
useEffect(() => {
fetchData(config) // Runs every render! config is new object
}, [config])
}
// Solution: Memoize the config
function UserDashboard({ userId }) {
const config = useMemo(() => ({
userId,
includeStats: true,
format: 'detailed',
}), [userId]) // Only changes when userId changes
useEffect(() => {
fetchData(config)
}, [config])
}Escape Hatch 2: Third-Party Libraries
Libraries without React Compiler support may expect stable references:
// Some charting libraries compare references
function Chart({ data }) {
// Ensure stable reference for library
const chartOptions = useMemo(() => ({
animation: true,
responsive: true,
data: transformData(data),
}), [data])
return <ThirdPartyChart options={chartOptions} />
}Escape Hatch 3: Expensive Computations
When you know a computation is expensive and want explicit control:
function SearchResults({ items, query }) {
// Explicitly expensive - want to ensure it's memoized
const filteredItems = useMemo(() => {
console.log('Filtering...')
return items
.filter(item => matchesQuery(item, query))
.sort(complexSortFn)
.slice(0, 100)
}, [items, query])
return <List items={filteredItems} />
}Escape Hatch 4: Referential Equality for Children
When passing objects/arrays to components that use referential equality:
function Parent() {
// Child component uses Object.is() comparison
const contextValue = useMemo(() => ({
theme: 'dark',
locale: 'en',
}), [])
return (
<MyContext.Provider value={contextValue}>
<Children />
</MyContext.Provider>
)
}When NOT to Use Escape Hatches
Don't Memoize Primitives
// ❌ Unnecessary - primitives are already stable
const memoizedId = useMemo(() => props.id, [props.id])
// ✅ Just use it directly
<Child id={props.id} />Don't Memoize Simple JSX
// ❌ Unnecessary with React Compiler
const memoizedButton = useMemo(() => (
<Button onClick={handleClick}>Click</Button>
), [handleClick])
// ✅ Compiler handles this
<Button onClick={handleClick}>Click</Button>Don't Memoize Everything "Just in Case"
// ❌ Over-memoization
function Component({ user }) {
const name = useMemo(() => user.name, [user.name])
const email = useMemo(() => user.email, [user.email])
const avatar = useMemo(() => user.avatar, [user.avatar])
return <Profile name={name} email={email} avatar={avatar} />
}
// ✅ Trust the compiler
function Component({ user }) {
return <Profile name={user.name} email={user.email} avatar={user.avatar} />
}useCallback Escape Hatches
Stable Event Handlers for Effects
function DataFetcher({ onDataLoaded }) {
// Need stable reference for effect dependency
const stableCallback = useCallback(
(data) => onDataLoaded(data),
[onDataLoaded]
)
useEffect(() => {
fetchData().then(stableCallback)
}, [stableCallback])
}Refs in Callbacks
function Form() {
const inputRef = useRef<HTMLInputElement>(null)
// Callback that uses ref - may need stability
const focusInput = useCallback(() => {
inputRef.current?.focus()
}, [])
return (
<>
<input ref={inputRef} />
<Button onClick={focusInput}>Focus</Button>
</>
)
}Decision Tree
Is it an effect dependency?
├─ YES → Does the effect need to run less often?
│ └─ YES → useMemo/useCallback
└─ NO → Is it passed to a third-party library?
├─ YES → Check library docs, may need useMemo
└─ NO → Is it a known expensive computation?
├─ YES → Consider useMemo for explicit control
└─ NO → Trust React CompilerVerifying Compiler Coverage
// In development, check DevTools for Memo badge
// If component doesn't have badge, compiler may have skipped it
// You can also add console logs to verify:
const value = useMemo(() => {
console.log('Computing...') // Should only log when deps change
return expensiveComputation()
}, [deps])React Compiler Migration Guide
Adopting React 19's automatic memoization.
What is React Compiler?
React Compiler automatically memoizes components and values, eliminating the need for manual useMemo, useCallback, and React.memo in most cases.
Prerequisites
- React 19+
- Compatible framework (Next.js 16+, Expo SDK 54+)
- Code follows Rules of React
Quick Setup
Next.js 16+
// next.config.js
const nextConfig = {
reactCompiler: true,
}
module.exports = nextConfigExpo SDK 54+
Enabled by default in new projects.
Babel (Manual)
npm install -D babel-plugin-react-compiler// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// Optional: sources to compile
sources: (filename) => {
return filename.indexOf('src') !== -1
},
}],
],
}Verification
1. Open React DevTools in browser 2. Go to Components tab 3. Look for "Memo ✨" badge next to component names 4. If you see the sparkle emoji, compiler is working
What Gets Optimized
The compiler automatically memoizes:
| Before (Manual) | After (Compiler) |
|---|---|
React.memo(Component) | Component re-renders only when needed |
useMemo(() => value, [deps]) | Intermediate values cached |
useCallback(() => fn, [deps]) | Callback references stable |
| Conditional JSX | JSX elements memoized |
Rules of React (Must Follow)
For the compiler to work correctly:
1. Components Must Be Idempotent
// ✅ Same input → same output
function Profile({ user }) {
return <h1>{user.name}</h1>
}
// ❌ Non-deterministic
function Profile({ user }) {
return <h1>{user.name} at {Date.now()}</h1>
}2. Props and State Are Immutable
// ✅ Create new object
setUser({ ...user, name: 'New Name' })
// ❌ Mutate existing
user.name = 'New Name'
setUser(user)3. Side Effects Outside Render
// ✅ In useEffect
useEffect(() => {
analytics.track('view')
}, [])
// ❌ During render
function Component() {
analytics.track('view') // BAD
return <div>...</div>
}4. Hooks at Top Level
// ✅ Always at top
function Component() {
const [state, setState] = useState()
// ...
}
// ❌ Conditional hooks
function Component({ show }) {
if (show) {
const [state, setState] = useState() // BAD
}
}Migration Strategy
New Projects
Enable compiler immediately. No reason not to.
Existing Projects
1. Enable compiler in config 2. Run tests to catch issues 3. Check DevTools for Memo badges 4. Gradually remove manual memoization
// Before (manual)
const MemoizedChild = React.memo(Child)
const memoizedValue = useMemo(() => compute(data), [data])
const handleClick = useCallback(() => onClick(id), [id, onClick])
// After (compiler handles it)
// Just use Child, compute(data), and onClick directly
// Compiler determines what needs memoizationWhen Manual Memoization Still Needed
Keep useMemo/useCallback for:
// 1. Effect dependencies that shouldn't trigger re-runs
const stableConfig = useMemo(() => ({
apiUrl: process.env.API_URL,
timeout: 5000,
}), [])
useEffect(() => {
initSDK(stableConfig) // Should only run once
}, [stableConfig])
// 2. Third-party libraries without compiler support
const memoizedData = useMemo(() =>
thirdPartyLib.transform(data), [data])
// 3. Precise control over boundaries
const handleSubmit = useCallback(async () => {
// Complex async logic that must be stable
}, [criticalDep])Debugging Issues
Component Not Getting Memo Badge
1. Check if file is in compiler's sources 2. Look for Rules of React violations 3. Check for unsupported patterns
Performance Regression
1. Profile with React DevTools 2. Check if compiler skipped problematic code 3. Add manual memoization as escape hatch
Compatibility Notes
- Works with existing
useMemo/useCallback(won't double-memoize) - Safe to leave existing memoization during migration
- Compiler output is equivalent to manual optimization
State Colocation
Keep state as close to where it's used as possible.
The Principle
State should live in the component that needs it. Only lift state when truly necessary for sibling communication.
Problem: State Too High
// ❌ State at app level causes unnecessary re-renders
function App() {
const [searchQuery, setSearchQuery] = useState('')
const [selectedId, setSelectedId] = useState(null)
return (
<div>
<Header /> {/* Re-renders on search! */}
<Sidebar /> {/* Re-renders on search! */}
<SearchInput
value={searchQuery}
onChange={setSearchQuery}
/>
<SearchResults
query={searchQuery}
selectedId={selectedId}
onSelect={setSelectedId}
/>
<Footer /> {/* Re-renders on search! */}
</div>
)
}Solution: Colocate State
// ✅ State colocated with components that use it
function App() {
return (
<div>
<Header />
<Sidebar />
<SearchSection /> {/* Contains its own state */}
<Footer />
</div>
)
}
function SearchSection() {
const [searchQuery, setSearchQuery] = useState('')
const [selectedId, setSelectedId] = useState(null)
return (
<>
<SearchInput
value={searchQuery}
onChange={setSearchQuery}
/>
<SearchResults
query={searchQuery}
selectedId={selectedId}
onSelect={setSelectedId}
/>
</>
)
}When to Lift State
Lift state ONLY when:
1. Siblings need to share it
// Both components need selectedUser
function Parent() {
const [selectedUser, setSelectedUser] = useState(null)
return (
<>
<UserList onSelect={setSelectedUser} selected={selectedUser} />
<UserDetails user={selectedUser} />
</>
)
}2. Parent needs to coordinate
// Parent manages form submission
function Form() {
const [values, setValues] = useState({})
const handleSubmit = () => {
api.submit(values)
}
return (
<>
<FormFields values={values} onChange={setValues} />
<SubmitButton onClick={handleSubmit} />
</>
)
}Component Splitting
Split components to isolate state:
// ❌ Before: Counter re-renders entire card
function Card() {
const [count, setCount] = useState(0)
return (
<div className="card">
<ExpensiveHeader /> {/* Re-renders on count change */}
<ExpensiveContent /> {/* Re-renders on count change */}
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
</div>
)
}
// ✅ After: Counter isolated
function Card() {
return (
<div className="card">
<ExpensiveHeader /> {/* Doesn't re-render */}
<ExpensiveContent /> {/* Doesn't re-render */}
<Counter /> {/* Only this re-renders */}
</div>
)
}
function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
)
}Context for Cross-Cutting Concerns
Use Context for truly global state, not local UI state:
// ✅ Good: Theme is app-wide
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
// ✅ Good: Auth is app-wide
<AuthContext.Provider value={user}>
<App />
</AuthContext.Provider>
// ❌ Bad: Search query is local
<SearchQueryContext.Provider value={query}> {/* Don't do this */}
<Header />
<SearchResults />
</SearchQueryContext.Provider>Context Splitting
Split contexts to prevent unnecessary re-renders:
// ❌ Single context - all consumers re-render
const AppContext = createContext({ user, theme, locale })
// ✅ Split contexts - targeted re-renders
const UserContext = createContext(null)
const ThemeContext = createContext('light')
const LocaleContext = createContext('en')Signs State Should Move
Move state DOWN when:
- Only one component uses it
- Child components don't need it
- Re-renders are affecting unrelated components
Move state UP when:
- Multiple children need to read it
- Children need to update each other
- State represents shared domain concept
Quick Checklist
- [ ] Is state used by only one component? → Keep it there
- [ ] Do siblings need this state? → Lift to parent
- [ ] Is it causing unnecessary re-renders? → Consider splitting
- [ ] Is it truly global? → Use Context
- [ ] Is it URL state? → Use router params
TanStack Virtual Patterns
Efficient virtualization for large lists and grids.
When to Virtualize
| Item Count | Recommendation |
|---|---|
| < 50 | Not needed |
| 50-100 | Consider if items are complex |
| 100-500 | Recommended |
| 500+ | Required |
Basic List Virtualization
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualList({ items }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Estimated row height in px
overscan: 5, // Render 5 extra items for smooth scrolling
})
return (
<div
ref={parentRef}
style={{ height: '400px', overflow: 'auto' }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
)
}Variable Height Rows
For rows with different heights:
function VariableHeightList({ items }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: (index) => {
// Return estimated height based on content
return items[index].type === 'header' ? 80 : 50
},
overscan: 5,
})
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement} // Enable dynamic measurement
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ItemComponent item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
)
}Dynamic Measurement
When content determines height:
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Initial estimate
// measureElement enables dynamic re-measurement
})
// Add ref to each item
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
>
{/* Content with unknown height */}
</div>Horizontal Virtualization
const columnVirtualizer = useVirtualizer({
horizontal: true,
count: columns.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 150, // Column width
overscan: 3,
})Grid Virtualization
Combine row and column virtualizers:
function VirtualGrid({ rows, columns }) {
const parentRef = useRef<HTMLDivElement>(null)
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
})
const columnVirtualizer = useVirtualizer({
horizontal: true,
count: columns.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
overscan: 3,
})
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: `${columnVirtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<React.Fragment key={virtualRow.key}>
{columnVirtualizer.getVirtualItems().map((virtualColumn) => (
<div
key={virtualColumn.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: `${virtualColumn.size}px`,
height: `${virtualRow.size}px`,
transform: `translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px)`,
}}
>
{/* Cell content */}
</div>
))}
</React.Fragment>
))}
</div>
</div>
)
}Scroll to Index
const virtualizer = useVirtualizer({/* ... */})
// Scroll to specific item
virtualizer.scrollToIndex(50, { align: 'start' })
// Align options: 'start' | 'center' | 'end' | 'auto'Window Scroller
For document-level scrolling:
import { useWindowVirtualizer } from '@tanstack/react-virtual'
function WindowList({ items }) {
const virtualizer = useWindowVirtualizer({
count: items.length,
estimateSize: () => 50,
overscan: 5,
})
return (
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
)
}Performance Tips
1. Use stable keys: Avoid array index as key 2. Memoize items: If item rendering is expensive 3. Adjust overscan: More overscan = smoother scroll, more DOM nodes 4. Measure sparingly: Only use measureElement when needed 5. Debounce scroll: For very heavy computations
// Optimized Context Pattern
// Split contexts to prevent unnecessary re-renders
import * as React from 'react'
// ============================================
// PATTERN 1: Split State and Dispatch
// ============================================
interface State {
count: number
user: { name: string } | null
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setUser'; payload: { name: string } | null }
// Separate contexts for state and dispatch
const StateContext = React.createContext<State | undefined>(undefined)
const DispatchContext = React.createContext<React.Dispatch<Action> | undefined>(undefined)
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 }
case 'decrement':
return { ...state, count: state.count - 1 }
case 'setUser':
return { ...state, user: action.payload }
default:
return state
}
}
export function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = React.useReducer(reducer, {
count: 0,
user: null,
})
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
)
}
// Hooks with proper error handling
export function useAppState() {
const context = React.useContext(StateContext)
if (context === undefined) {
throw new Error('useAppState must be used within AppProvider')
}
return context
}
export function useAppDispatch() {
const context = React.useContext(DispatchContext)
if (context === undefined) {
throw new Error('useAppDispatch must be used within AppProvider')
}
return context
}
// ============================================
// PATTERN 2: Selective Subscriptions
// ============================================
interface StoreState {
theme: 'light' | 'dark'
locale: string
user: { id: string; name: string } | null
}
// Create separate contexts for each piece of state
const ThemeContext = React.createContext<'light' | 'dark'>('light')
const LocaleContext = React.createContext<string>('en')
const UserContext = React.createContext<{ id: string; name: string } | null>(null)
// Setters in separate context
interface StoreSetters {
setTheme: (theme: 'light' | 'dark') => void
setLocale: (locale: string) => void
setUser: (user: { id: string; name: string } | null) => void
}
const SettersContext = React.createContext<StoreSetters | undefined>(undefined)
export function StoreProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = React.useState<'light' | 'dark'>('light')
const [locale, setLocale] = React.useState('en')
const [user, setUser] = React.useState<{ id: string; name: string } | null>(null)
// Memoize setters to prevent re-renders
const setters = React.useMemo(
() => ({ setTheme, setLocale, setUser }),
[]
)
return (
<SettersContext.Provider value={setters}>
<ThemeContext.Provider value={theme}>
<LocaleContext.Provider value={locale}>
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
</LocaleContext.Provider>
</ThemeContext.Provider>
</SettersContext.Provider>
)
}
// Hooks for selective subscription
export function useTheme() {
return React.useContext(ThemeContext)
}
export function useLocale() {
return React.useContext(LocaleContext)
}
export function useUser() {
return React.useContext(UserContext)
}
export function useStoreSetters() {
const context = React.useContext(SettersContext)
if (context === undefined) {
throw new Error('useStoreSetters must be used within StoreProvider')
}
return context
}
// ============================================
// PATTERN 3: Stable Value with useMemo
// ============================================
interface AuthContextValue {
user: { id: string; name: string } | null
isAuthenticated: boolean
login: (credentials: { email: string; password: string }) => Promise<void>
logout: () => void
}
const AuthContext = React.createContext<AuthContextValue | undefined>(undefined)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = React.useState<{ id: string; name: string } | null>(null)
// Memoize callbacks
const login = React.useCallback(async (credentials: { email: string; password: string }) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
})
const data = await response.json()
setUser(data.user)
}, [])
const logout = React.useCallback(() => {
setUser(null)
}, [])
// Memoize entire context value
const value = React.useMemo(
() => ({
user,
isAuthenticated: user !== null,
login,
logout,
}),
[user, login, logout]
)
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = React.useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
// Usage:
/*
// Pattern 1 - State/Dispatch split
function Counter() {
const { count } = useAppState() // Re-renders only when count changes
return <span>{count}</span>
}
function IncrementButton() {
const dispatch = useAppDispatch() // Never re-renders (dispatch is stable)
return <button onClick={() => dispatch({ type: 'increment' })}>+</button>
}
// Pattern 2 - Selective subscriptions
function ThemeToggle() {
const theme = useTheme() // Only re-renders when theme changes
const { setTheme } = useStoreSetters() // Stable reference
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>
}
// Pattern 3 - Stable context value
function UserProfile() {
const { user, logout } = useAuth()
// Only re-renders when user changes
return user ? <button onClick={logout}>Logout {user.name}</button> : null
}
*/
// Virtualized List Template with TanStack Virtual
// Copy and customize for your project
import * as React from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import { cn } from '@/lib/utils'
// Types
interface ListItem {
id: string
[key: string]: unknown
}
interface VirtualizedListProps<T extends ListItem> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
estimateSize?: number
overscan?: number
className?: string
itemClassName?: string
height?: number | string
}
// Basic Virtualized List
export function VirtualizedList<T extends ListItem>({
items,
renderItem,
estimateSize = 50,
overscan = 5,
className,
itemClassName,
height = 400,
}: VirtualizedListProps<T>) {
const parentRef = React.useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimateSize,
overscan,
})
return (
<div
ref={parentRef}
className={cn('overflow-auto', className)}
style={{ height }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={items[virtualItem.index].id}
className={itemClassName}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{renderItem(items[virtualItem.index], virtualItem.index)}
</div>
))}
</div>
</div>
)
}
// Dynamic Height List (measures actual content)
export function DynamicVirtualizedList<T extends ListItem>({
items,
renderItem,
estimateSize = 50,
overscan = 5,
className,
height = 400,
}: VirtualizedListProps<T>) {
const parentRef = React.useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimateSize,
overscan,
})
return (
<div
ref={parentRef}
className={cn('overflow-auto', className)}
style={{ height }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={items[virtualItem.index].id}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
{renderItem(items[virtualItem.index], virtualItem.index)}
</div>
))}
</div>
</div>
)
}
// Usage Examples:
/*
import { VirtualizedList, DynamicVirtualizedList } from './virtualized-list'
// Basic usage
const items = Array.from({ length: 10000 }, (_, i) => ({
id: `item-${i}`,
name: `Item ${i}`,
}))
<VirtualizedList
items={items}
renderItem={(item) => (
<div className="p-4 border-b">{item.name}</div>
)}
height={500}
estimateSize={48}
/>
// Dynamic heights
<DynamicVirtualizedList
items={posts}
renderItem={(post) => (
<div className="p-4 border-b">
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
)}
/>
// With custom styling
<VirtualizedList
items={items}
renderItem={(item, index) => (
<div className={cn(
'p-4 border-b',
index % 2 === 0 ? 'bg-gray-50' : 'bg-white'
)}>
{item.name}
</div>
)}
className="border rounded-lg"
height="calc(100vh - 200px)"
/>
*/