React performance optimization patterns using memoization, code splitting, and efficient rendering strategies. Use when optimizing slow React applications, reducing bundle size, or improving user experience with large datasets.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Expert guidance for optimizing React application performance through memoization, code splitting, virtualization, and efficient rendering strategies for building fast, responsive user interfaces.
Prevent unnecessary re-renders of functional components:
import React, { memo } from 'react';
const ExpensiveComponent = memo(({ data, onAction }) => {
console.log('Rendering ExpensiveComponent');
return (
<div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<button onClick={onAction}>Action</button>
</div>
);
});
// Custom comparison for complex props
const UserCard = memo(
({ user, settings }) => (
<div>
<h2>{user.name}</h2>
<span>{user.email}</span>
</div>
),
(prevProps, nextProps) => {
// Return true if props are equal (skip render)
return prevProps.user.id === nextProps.user.id &&
prevProps.settings.theme === nextProps.settings.theme;
}
);
When to use:
When NOT to use:
Cache expensive calculation results:
import { useMemo } from 'react';
function DataAnalyzer({ items, filters }) {
// Recalculates only when items or filters change
const filteredAndSorted = useMemo(() => {
console.log('Computing filtered data');
return items
.filter(item => filters.categories.includes(item.category))
.filter(item => item.price >= filters.minPrice)
.sort((a, b) => b.score - a.score);
}, [items, filters]);
const statistics = useMemo(() => {
return {
total: filteredAndSorted.length,
average: filteredAndSorted.reduce((sum, item) => sum + item.price, 0) /
filteredAndSorted.length,
maxPrice: Math.max(...filteredAndSorted.map(item => item.price))
};
}, [filteredAndSorted]);
return (
<div>
<p>Total items: {statistics.total}</p>
<p>Average price: ${statistics.average.toFixed(2)}</p>
</div>
);
}
Use cases:
Performance impact:
Prevent child re-renders caused by function reference changes:
import { useState, useCallback, memo } from 'react';
const ListItem = memo(({ item, onDelete, onEdit }) => {
console.log('Rendering ListItem:', item.id);
return (
<div>
<span>{item.name}</span>
<button onClick={() => onEdit(item.id)}>Edit</button>
<button onClick={() => onDelete(item.id)}>Delete</button>
</div>
);
});
function ItemList({ items }) {
const [selectedId, setSelectedId] = useState(null);
// Stable function reference across renders
const handleDelete = useCallback((id) => {
console.log('Deleting:', id);
// API call to delete
}, []); // No dependencies = never recreated
const handleEdit = useCallback((id) => {
setSelectedId(id);
// Open edit modal
}, [setSelectedId]); // Recreated only if setSelectedId changes
return (
<div>
{items.map(item => (
<ListItem
key={item.id}
item={item}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</div>
);
}
Critical rule:
useCallback when passing functions to memoized child componentsLoad components on demand for smaller initial bundles:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy-loaded route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Reports = lazy(() => import('./pages/Reports'));
const Settings = lazy(() => import('./pages/Settings'));
// Component-level code splitting
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
function DataVisualization({ data, showChart }) {
return (
<div>
<h2>Data Overview</h2>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart data={data} />
</Suspense>
)}
</div>
);
}
Benefits:
Best practices:
<link rel="preload">Render only visible items to handle thousands of rows:
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style} className="list-item">
<h4>{items[index].title}</h4>
<p>{items[index].description}</p>
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={80}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Variable size list (different heights)
import { VariableSizeList } from 'react-window';
function DynamicList({ items }) {
const getItemSize = (index) => {
return items[index].type === 'header' ? 60 : 40;
};
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].content}</div>
)}
</VariableSizeList>
);
}
Performance impact:
Libraries:
react-window: Lightweight, simple API (recommended)react-virtualized: Feature-rich, larger bundle@tanstack/react-virtual: Modern, headless virtualizationProper keys prevent unnecessary re-renders:
// BAD: Index as key (breaks when reordering/filtering)
{items.map((item, index) => (
<Item key={index} data={item} />
))}
// BAD: Random keys (forces complete re-render every time)
{items.map(item => (
<Item key={Math.random()} data={item} />
))}
// GOOD: Stable unique identifier
{items.map(item => (
<Item key={item.id} data={item} />
))}
// GOOD: Composite key when no unique ID exists
{items.map(item => (
<Item key={`${item.userId}-${item.timestamp}`} data={item} />
))}
Why keys matter:
Optimize state structure to minimize re-renders:
import { useState, createContext, useContext } from 'react';
// BAD: Single large state object causes many re-renders
function BadApp() {
const [state, setState] = useState({
user: {},
settings: {},
data: [],
ui: { modal: false, sidebar: true }
});
// Changing modal state re-renders entire tree
const toggleModal = () => setState(prev => ({
...prev,
ui: { ...prev.ui, modal: !prev.ui.modal }
}));
}
// GOOD: Split state by update frequency
function GoodApp() {
const [user, setUser] = useState({});
const [settings, setSettings] = useState({});
const [data, setData] = useState([]);
const [modalOpen, setModalOpen] = useState(false);
// Only components using modalOpen re-render
}
// BEST: Context splitting for shared state
const UserContext = createContext();
const DataContext = createContext();
function App() {
const [user, setUser] = useState({});
const [data, setData] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<DataContext.Provider value={{ data, setData }}>
<Dashboard />
</DataContext.Provider>
</UserContext.Provider>
);
}
// Components only subscribe to needed context
function UserProfile() {
const { user } = useContext(UserContext); // Only re-renders on user change
return <div>{user.name}</div>;
}
State management strategies:
Reduce bundle size with smart imports and tree shaking:
// BAD: Imports entire library
import _ from 'lodash';
import { Button, Modal, Table, Form } from 'antd';
// GOOD: Import only needed functions
import debounce from 'lodash/debounce';
import groupBy from 'lodash/groupBy';
// GOOD: Tree-shakeable imports (if library supports it)
import { Button } from 'antd/es/button';
import { Modal } from 'antd/es/modal';
// Dynamic imports for heavy libraries
const PDFViewer = lazy(() => import('react-pdf-viewer'));
const CodeEditor = lazy(() => import('@monaco-editor/react'));
// Conditional polyfill loading
async function loadPolyfills() {
if (!window.IntersectionObserver) {
await import('intersection-observer');
}
}
Bundle analysis tools:
# Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
# Vite Bundle Visualizer
npm install --save-dev rollup-plugin-visualizer
# Analyze bundle composition
npm run build -- --stats
npx webpack-bundle-analyzer dist/stats.json
Identify and diagnose performance bottlenecks:
import { Profiler } from 'react';
function onRenderCallback(
id, // Component being profiled
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime, // When render started
commitTime, // When committed to DOM
interactions // Set of interactions
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
// Send to analytics
if (actualDuration > 16) { // More than one frame (60fps)
sendToAnalytics({ id, phase, actualDuration });
}
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
<Profiler id="Sidebar" onRender={onRenderCallback}>
<Sidebar />
</Profiler>
</Profiler>
);
}
DevTools Profiler workflow:
Leverage concurrent rendering for better responsiveness:
import { useState, useTransition, useDeferredValue } from 'react';
// useTransition: Mark non-urgent updates
function SearchApp() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
setQuery(value); // Urgent: Update input immediately
startTransition(() => {
// Non-urgent: Can be interrupted
setResults(searchItems(value));
});
};
return (
<div>
<input value={query} onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}
// useDeferredValue: Defer expensive renders
function FilteredList({ items, searchTerm }) {
const deferredSearchTerm = useDeferredValue(searchTerm);
// Filters using deferred value (doesn't block typing)
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(deferredSearchTerm.toLowerCase())
);
}, [items, deferredSearchTerm]);
return (
<div>
<p>Showing {filteredItems.length} results</p>
{filteredItems.map(item => <Item key={item.id} data={item} />)}
</div>
);
}
Concurrent features benefits:
Measure first, optimize second:
// Use React DevTools Profiler to find actual bottlenecks
// Don't guess - measure!
// Browser Performance API for custom measurements
performance.mark('render-start');
// ... component render logic ...
performance.mark('render-end');
performance.measure('component-render', 'render-start', 'render-end');
const measure = performance.getEntriesByName('component-render')[0];
console.log(`Render took: ${measure.duration}ms`);
Optimization hierarchy:
1. Make it work (correctness first)
2. Measure performance (identify real bottlenecks)
3. Optimize hot paths (only slow parts)
4. Measure again (verify improvement)
Common pitfalls:
Control re-computation with proper dependencies:
// BAD: Missing dependencies (stale closures)
const fetchData = useCallback(() => {
fetch(`/api/data?filter=${filter}`);
}, []); // Missing filter dependency
// BAD: Object/array dependencies (always new reference)
const config = { url: '/api', filter };
useEffect(() => {
fetchData(config);
}, [config]); // New object every render
// GOOD: Primitive dependencies
useEffect(() => {
fetchData({ url: '/api', filter });
}, [filter, fetchData]);
// GOOD: Stable reference with useMemo
const config = useMemo(() => ({ url: '/api', filter }), [filter]);
Lazy load images and use modern formats:
// Native lazy loading
function ImageGallery({ images }) {
return (
<div>
{images.map(img => (
<img
key={img.id}
src={img.url}
loading="lazy"
alt={img.alt}
decoding="async"
/>
))}
</div>
);
}
// Intersection Observer for custom loading
import { useEffect, useRef, useState } from 'react';
function LazyImage({ src, alt }) {
const [isLoaded, setIsLoaded] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsLoaded(true);
observer.disconnect();
}
},
{ rootMargin: '50px' }
);
if (imgRef.current) observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={isLoaded ? src : '/placeholder.jpg'}
alt={alt}
/>
);
}
// BAD: New object every render defeats memo
<Component config={{ theme: 'dark' }} />
// GOOD: Stable reference
const config = useMemo(() => ({ theme: 'dark' }), []);
<Component config={config} />
// BEST: Extract to constant if truly static
const CONFIG = { theme: 'dark' };
<Component config={CONFIG} />
// BAD: New function every render
<button onClick={() => handleClick(id)}>Click</button>
// GOOD: useCallback with stable reference
const handleButtonClick = useCallback(() => handleClick(id), [id]);
<button onClick={handleButtonClick}>Click</button>
// ACCEPTABLE: For top-level handlers (not passed to memoized children)
<button onClick={(e) => console.log(e.target.value)}>Click</button>
// BAD: Duplicate state causes sync issues
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);
// GOOD: Derive during render
const [items, setItems] = useState([]);
const itemCount = items.length; // Always in sync
// BAD: Unnecessary memoization adds overhead
const SimpleComponent = memo(({ text }) => <span>{text}</span>);
// GOOD: Only memoize if expensive or frequently re-rendered with same props
const ExpensiveComponent = memo(({ data }) => {
// Complex rendering logic
return <ComplexVisualization data={processData(data)} />;
});