React Performance
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: react topic: performance for comprehensive documentation on React performance optimization and profiling techniques.
Full Reference: See advanced.md for Profiling with React DevTools, Bundle Optimization, Image Optimization, and Web Workers.
Memoization
React.memo
Prevents re-renders when props haven't changed:
// Only re-renders when props change (shallow comparison) const ExpensiveComponent = memo(function ExpensiveComponent({ data, onItemClick, }: Props) { return ( <ul> {data.map(item => ( <li key={item.id} onClick={() => onItemClick(item.id)}> {item.name} </li> ))} </ul> ); });
// With custom comparison const OptimizedComponent = memo( function OptimizedComponent({ user }: Props) { return <div>{user.name}</div>; }, (prevProps, nextProps) => prevProps.user.id === nextProps.user.id );
When to Use memo
// ✅ Good: Expensive component with stable parent const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) { return items.map(item => <ExpensiveItem key={item.id} item={item} />); });
// ❌ Bad: Simple component, memo overhead not worth it const SimpleText = memo(function SimpleText({ text }: { text: string }) { return <span>{text}</span>; });
// ❌ Bad: Props always change anyway function Parent() { // New object on every render - memo is useless! return <MemoizedChild data={{ value: 1 }} />; }
useMemo
Memoize expensive calculations:
function ProductList({ products, filter }: Props) { const filteredProducts = useMemo(() => { return products .filter(p => p.category === filter.category) .filter(p => p.price >= filter.minPrice && p.price <= filter.maxPrice) .sort((a, b) => a.name.localeCompare(b.name)); }, [products, filter.category, filter.minPrice, filter.maxPrice]);
return ( <ul> {filteredProducts.map(p => <ProductCard key={p.id} product={p} />)} </ul> ); }
// ❌ Don't overuse - simple operations don't need memoization const total = useMemo(() => a + b, [a, b]); // Overkill!
useCallback
Memoize functions to prevent child re-renders:
function Parent() { const [count, setCount] = useState(0);
// ✅ Stable reference for child components const handleClick = useCallback((id: string) => { console.log('Clicked:', id); }, []);
// ✅ Dependencies that change function behavior const handleSubmit = useCallback((data: FormData) => { submitWithCount(data, count); }, [count]);
return ( <> <ChildComponent onClick={handleClick} /> <Form onSubmit={handleSubmit} /> </> ); }
Virtualization
Render only visible items for large lists:
import { FixedSizeList } from 'react-window';
function VirtualList({ items }: { items: Item[] }) { const Row = ({ index, style }: { index: number; style: CSSProperties }) => ( <div style={style} className="list-item"> {items[index].name} </div> );
return ( <FixedSizeList height={400} width="100%" itemCount={items.length} itemSize={50} > {Row} </FixedSizeList> ); }
With TanStack Virtual
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) { const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, overscan: 5, });
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: translateY(${virtualItem.start}px),
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
Code Splitting
Route-based Splitting
import { lazy, Suspense } from 'react'; import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home')); const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings'));
function App() { return ( <Suspense fallback={<PageLoader />}> <Routes> <Route path="/" element={<Home />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); }
Component-based Splitting
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() { const [showChart, setShowChart] = useState(false);
return ( <div> <button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={data} />
</Suspense>
)}
</div>
); }
Preloading
const Dashboard = lazy(() => import('./Dashboard')); const preloadDashboard = () => import('./Dashboard');
function NavLink() { return ( <Link to="/dashboard" onMouseEnter={preloadDashboard} onFocus={preloadDashboard} > Dashboard </Link> ); }
Avoiding Unnecessary Re-renders
Stable References
// ❌ Bad: New object on every render function Parent() { return <Child style={{ color: 'red' }} />; }
// ✅ Good: Stable reference const style = { color: 'red' }; function Parent() { return <Child style={style} />; }
// ✅ Good: useMemo for dynamic values function Parent({ color }) { const style = useMemo(() => ({ color }), [color]); return <Child style={style} />; }
Component Composition
// ❌ Bad: Entire component re-renders on count change function App() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(c => c + 1)}>{count}</button> <ExpensiveComponent /> {/* Re-renders on every count change! */} </div> ); }
// ✅ Good: Move state down function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }
function App() { return ( <div> <Counter /> <ExpensiveComponent /> {/* Doesn't re-render! */} </div> ); }
// ✅ Good: Pass children as props function Counter({ children }) { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(c => c + 1)}>{count}</button> {children} {/* Doesn't re-render! */} </div> ); }
State Management Optimization
Batching Updates
// React 18+ automatically batches these function handleClick() { setCount(c => c + 1); setFlag(f => !f); setText('updated'); // Only ONE re-render! }
Derived State
// ❌ Bad: Synchronized state function Form() { const [items, setItems] = useState([]); const [total, setTotal] = useState(0);
useEffect(() => { setTotal(items.reduce((sum, item) => sum + item.price, 0)); }, [items]); // Extra re-render! }
// ✅ Good: Calculate during render function Form() { const [items, setItems] = useState([]); const total = items.reduce((sum, item) => sum + item.price, 0); }
// ✅ Good: useMemo for expensive calculations function Form() { const [items, setItems] = useState([]); const total = useMemo( () => items.reduce((sum, item) => sum + item.price, 0), [items] ); }
Common Pitfalls
Issue Cause Solution
Slow initial render Large bundle Code splitting, lazy loading
Slow updates Too many re-renders memo, useMemo, useCallback
Janky scrolling Rendering all list items Virtualization
Memory leaks Uncleaned effects Proper cleanup functions
Layout thrashing Forced synchronous layout Batch DOM reads/writes
Best Practices
-
✅ Profile before optimizing
-
✅ Use React DevTools Profiler
-
✅ Virtualize long lists (>100 items)
-
✅ Code split at route level
-
✅ Memoize expensive computations
-
✅ Use stable references for objects/functions
-
❌ Don't over-optimize prematurely
-
❌ Don't wrap everything in memo
-
❌ Don't block main thread with heavy JS
When NOT to Use This Skill
-
React 19 Compiler optimization - Use react-19 skill for compiler-specific features
-
Basic React development - Use react skill for general component development
-
Testing - Use react-testing skill for performance testing strategies
-
Non-performance React issues - This skill is specifically for optimization
Anti-Patterns
Anti-Pattern Problem Solution
Premature optimization Wasted effort, complex code Profile first, optimize what matters
Wrapping everything in memo Overhead, no benefit Only memoize expensive components
useMemo for cheap calculations More overhead than savings Only memoize expensive operations
Inline object/function in JSX Breaks child memoization Extract to constant or useCallback
Not using key prop correctly Full list re-render Use stable unique IDs
Rendering entire list Slow scrolling Use virtualization for >100 items
Large bundle without code splitting Slow initial load Split by routes
Quick Troubleshooting
Issue Likely Cause Fix
Slow initial load Large bundle size Code split, lazy load routes
Janky scrolling Rendering all list items Use react-window or TanStack Virtual
Frequent unnecessary re-renders Props changing identity Memoize objects/functions with useMemo/useCallback
Slow component updates Heavy computation in render Move to useMemo or Web Worker
Memory leaks Uncleaned effects/subscriptions Add cleanup functions to useEffect
Large JavaScript payload Not tree-shaking Import only what you need
Reference Documentation
-
React Profiler
-
Performance Optimization
-
MCP: mcp__documentation__fetch_docs → technology: react , topic: performance