
React Performance Optimization
- 5 installs
- 28 repo stars
- Updated June 29, 2026
- nickcrew/claude-cortex
This is a copy of react-performance-optimization by nickcrew - installs and ranking accrue to the original listing.
Helps with frontend development tasks.
About
react-performance-optimization is a Claude Code skill in the Frontend Development category.
- react-performance-optimization
- Frontend Development
- AI-coding skill
React Performance Optimization by the numbers
- 5 all-time installs (skills.sh)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nickcrew/claude-cortex --skill react-performance-optimizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 28 |
| Last updated | June 29, 2026 |
| Repository | nickcrew/claude-cortex ↗ |
What it does
Helps with frontend development tasks.
Files
React Performance Optimization
Expert guidance for optimizing React application performance through memoization, code splitting, virtualization, and efficient rendering strategies.
When to Use This Skill
- Optimizing slow-rendering React components
- Reducing bundle size for faster initial load times
- Improving responsiveness for large lists or data tables
- Preventing unnecessary re-renders in complex component trees
- Optimizing state management to reduce render cascades
- Improving perceived performance with code splitting
- Debugging performance issues with React DevTools Profiler
Core Concepts
React Rendering Optimization
React re-renders components when props or state change. Unnecessary re-renders waste CPU cycles and degrade user experience. Key optimization techniques:
- Memoization: Cache component renders and computed values
- Code splitting: Load code on demand for faster initial loads
- Virtualization: Render only visible list items
- State optimization: Structure state to minimize render cascades
When to Optimize
1. Profile first: Use React DevTools Profiler to identify actual bottlenecks 2. Measure impact: Verify optimization improves performance 3. Avoid premature optimization: Don't optimize fast components
Quick Reference
Load detailed patterns and examples as needed:
| Topic | Reference File |
|---|---|
| React.memo, useMemo, useCallback patterns | skills/react-performance-optimization/references/memoization.md |
| Code splitting with lazy/Suspense, bundle optimization | skills/react-performance-optimization/references/code-splitting.md |
| Virtualization for large lists (react-window) | skills/react-performance-optimization/references/virtualization.md |
| State management strategies, context splitting | skills/react-performance-optimization/references/state-management.md |
| useTransition, useDeferredValue (React 18+) | skills/react-performance-optimization/references/concurrent-features.md |
| React DevTools Profiler, performance monitoring | skills/react-performance-optimization/references/profiling-debugging.md |
| Common pitfalls and anti-patterns | skills/react-performance-optimization/references/common-pitfalls.md |
Optimization Workflow
1. Identify Bottlenecks
# Open React DevTools Profiler
# Record interaction → Analyze flame graph → Find slow componentsLook for:
- Components with yellow/red bars (slow renders)
- Unnecessary renders (same props/state)
- Expensive computations on every render
2. Apply Targeted Optimizations
For unnecessary re-renders:
- Wrap component with
React.memo - Use
useCallbackfor stable function references - Check for inline objects/arrays in props
For expensive computations:
- Use
useMemoto cache results - Move calculations outside render when possible
For large lists:
- Implement virtualization with react-window
- Ensure proper unique keys (not index)
For slow initial load:
- Add code splitting with
React.lazy - Analyze bundle size with webpack-bundle-analyzer
- Use dynamic imports for heavy dependencies
3. Verify Improvements
# Record new Profiler session
# Compare before/after metrics
# Ensure optimization actually helpedCommon Patterns
Memoize Expensive Components
import { memo } from 'react';
const ExpensiveList = memo(({ items, onItemClick }) => {
return items.map(item => (
<Item key={item.id} data={item} onClick={onItemClick} />
));
});Cache Computed Values
import { useMemo } from 'react';
function DataTable({ items, filters }) {
const filteredItems = useMemo(() => {
return items.filter(item => filters.includes(item.category));
}, [items, filters]);
return <Table data={filteredItems} />;
}Stable Function References
import { useCallback } from 'react';
function Parent() {
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []);
return <MemoizedChild onClick={handleClick} />;
}Code Split Routes
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Reports = lazy(() => import('./Reports'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}Virtualize Large Lists
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={80}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}Common Mistakes
1. Over-memoization: Don't memoize simple, fast components (adds overhead) 2. Inline objects/arrays: New references break memoization (config={{ theme: 'dark' }}) 3. Missing dependencies: Stale closures in useCallback/useMemo 4. Index as key: Breaks reconciliation when list order changes 5. Single large context: Causes widespread re-renders on any update 6. No profiling: Optimizing without measuring wastes time
Performance Checklist
Before optimizing:
- [ ] Profile with React DevTools to identify bottlenecks
- [ ] Measure baseline performance metrics
Optimization targets:
- [ ] Memoize expensive components with stable props
- [ ] Cache computed values with useMemo (if actually expensive)
- [ ] Use useCallback for functions passed to memoized children
- [ ] Implement code splitting for routes and heavy components
- [ ] Virtualize lists with >100 items
- [ ] Provide stable keys for list items (unique IDs, not index)
- [ ] Split state by update frequency
- [ ] Use concurrent features (useTransition, useDeferredValue) for responsiveness
After optimizing:
- [ ] Profile again to verify improvements
- [ ] Check bundle size reduction (if applicable)
- [ ] Ensure no regressions in functionality
Resources
- React Docs - Performance: https://react.dev/learn/render-and-commit
- React DevTools: Browser extension for profiling
- react-window: https://github.com/bvaughn/react-window
- Bundle analyzers: webpack-bundle-analyzer, rollup-plugin-visualizer
- Lighthouse: Chrome DevTools performance audit
Code Splitting Patterns
React.lazy and Suspense
Load 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:
- Reduces initial bundle size (faster First Contentful Paint)
- Loads code only when needed (better caching)
- Route-based splitting: Users only download visited pages
Best practices:
- Split by routes first (biggest impact)
- Split heavy components (charts, editors, modals)
- Provide meaningful loading fallbacks
- Preload critical routes with
<link rel="preload">
Bundle Optimization
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.jsonAnalysis workflow: 1. Generate production build with stats 2. Open bundle visualizer 3. Identify large dependencies 4. Check for duplicate code 5. Find optimization opportunities (lazy loading, tree shaking) 6. Measure improvement after changes
Common Performance Pitfalls
1. Inline Object/Array Props
The Problem
// BAD: New object every render defeats memo
function Parent() {
return <Component config={{ theme: 'dark' }} />;
}
const Component = memo(({ config }) => {
// Re-renders every time because config is a new object
return <div>{config.theme}</div>;
});Solutions
// GOOD: Stable reference with useMemo
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), []);
return <Component config={config} />;
}
// BEST: Extract to constant if truly static
const CONFIG = { theme: 'dark' };
function Parent() {
return <Component config={CONFIG} />;
}
// ALSO GOOD: Pass primitives directly
function Parent() {
return <Component theme="dark" />;
}2. Anonymous Functions in JSX
The Problem
// BAD: New function every render
function List({ items }) {
return items.map(item => (
<Item
key={item.id}
onClick={() => handleClick(item.id)}
/>
));
}Solutions
// GOOD: useCallback with stable reference
function List({ items }) {
const handleItemClick = useCallback((id) => {
handleClick(id);
}, []);
return items.map(item => (
<Item
key={item.id}
onClick={() => handleItemClick(item.id)}
/>
));
}
// ACCEPTABLE: For top-level handlers (not passed to memoized children)
function Form() {
return (
<button onClick={(e) => console.log(e.target.value)}>
Click
</button>
);
}3. Over-Memoization
The Problem
// BAD: Unnecessary memoization adds overhead
const SimpleComponent = memo(({ text }) => <span>{text}</span>);
const number = useMemo(() => 2 + 2, []); // Pointless
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // Only useful if passed to memoized childWhen to Memoize
// GOOD: Only memoize if expensive or frequently re-rendered with same props
const ExpensiveComponent = memo(({ data }) => {
// Complex rendering logic
const processed = processLargeDataset(data);
return <ComplexVisualization data={processed} />;
});
// GOOD: useMemo for actual expensive computations
const sortedData = useMemo(() => {
return largeArray.sort((a, b) => b.score - a.score);
}, [largeArray]);
// GOOD: useCallback when passing to memoized children
const MemoizedChild = memo(ChildComponent);
function Parent() {
const handleAction = useCallback(() => {
// handler logic
}, []);
return <MemoizedChild onAction={handleAction} />;
}4. Deriving State Unnecessarily
The Problem
// BAD: Duplicate state causes sync issues
function BadComponent({ items }) {
const [itemsState, setItemsState] = useState(items);
const [itemCount, setItemCount] = useState(items.length);
// Easy to forget to update itemCount
const addItem = (item) => {
setItemsState([...itemsState, item]);
setItemCount(itemCount + 1); // Can get out of sync
};
}Solutions
// GOOD: Derive during render
function GoodComponent({ items }) {
const [itemsState, setItemsState] = useState(items);
const itemCount = itemsState.length; // Always in sync
const addItem = (item) => {
setItemsState([...itemsState, item]);
};
}
// GOOD: useMemo for expensive derivations only
function ComponentWithExpensiveCalc({ items }) {
const statistics = useMemo(() => {
return {
count: items.length,
total: items.reduce((sum, item) => sum + item.value, 0),
average: items.reduce((sum, item) => sum + item.value, 0) / items.length
};
}, [items]);
}5. Incorrect Dependencies
The Problem
// BAD: Missing dependencies (stale closures)
function Search() {
const [filter, setFilter] = useState('');
const fetchData = useCallback(() => {
fetch(`/api/data?filter=${filter}`);
}, []); // Missing filter dependency!
}
// BAD: Object/array dependencies (always new reference)
function DataComponent() {
const config = { url: '/api', filter };
useEffect(() => {
fetchData(config);
}, [config]); // New object every render = runs every render
}Solutions
// GOOD: Include all dependencies
function Search() {
const [filter, setFilter] = useState('');
const fetchData = useCallback(() => {
fetch(`/api/data?filter=${filter}`);
}, [filter]); // Includes filter
}
// GOOD: Primitive dependencies
function DataComponent() {
const [filter, setFilter] = useState('');
useEffect(() => {
fetchData({ url: '/api', filter });
}, [filter]); // Only primitive
}
// GOOD: Stable reference with useMemo
function DataComponent() {
const [filter, setFilter] = useState('');
const config = useMemo(() => ({ url: '/api', filter }), [filter]);
useEffect(() => {
fetchData(config);
}, [config]);
}6. Context Performance Issues
The Problem
// BAD: Single context with everything causes widespread re-renders
const AppContext = createContext();
function App() {
const [user, setUser] = useState({});
const [theme, setTheme] = useState('light');
const [data, setData] = useState([]);
return (
<AppContext.Provider value={{ user, theme, data, setUser, setTheme, setData }}>
<Dashboard />
</AppContext.Provider>
);
}
// Every component re-renders when ANY value changesSolutions
// GOOD: Split contexts by update frequency
const UserContext = createContext();
const ThemeContext = createContext();
const DataContext = createContext();
function App() {
const [user, setUser] = useState({});
const [theme, setTheme] = useState('light');
const [data, setData] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<DataContext.Provider value={{ data, setData }}>
<Dashboard />
</DataContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// Components only re-render when their specific context changes
function UserProfile() {
const { user } = useContext(UserContext); // Only re-renders on user change
return <div>{user.name}</div>;
}7. Large Component Files
The Problem
- Difficult to optimize specific parts
- Hard to identify performance bottlenecks
- Monolithic re-renders
Solution
// Split into smaller, focused components
// Each can be optimized independently
// Before: One large component
function Dashboard() {
return (
<div>
{/* 500 lines of JSX */}
</div>
);
}
// After: Focused components
function Dashboard() {
return (
<div>
<Header />
<Sidebar />
<MainContent />
<Footer />
</div>
);
}
const Header = memo(HeaderComponent);
const Sidebar = memo(SidebarComponent);
const MainContent = memo(MainContentComponent);8. Image Loading Issues
The Problem
// BAD: All images load immediately
function Gallery({ images }) {
return (
<div>
{images.map(img => (
<img key={img.id} src={img.url} alt={img.alt} />
))}
</div>
);
}Solutions
// GOOD: Native lazy loading
function Gallery({ images }) {
return (
<div>
{images.map(img => (
<img
key={img.id}
src={img.url}
alt={img.alt}
loading="lazy"
decoding="async"
/>
))}
</div>
);
}
// BETTER: 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}
/>
);
}Concurrent Features (React 18+)
useTransition
Mark non-urgent updates for better responsiveness:
import { useState, useTransition } from 'react';
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>
);
}When to use:
- Search filtering with live results
- Tab switching with heavy content
- Any UI update that can be delayed for responsiveness
useDeferredValue
Defer expensive renders without explicit transitions:
import { useState, useDeferredValue, useMemo } from 'react';
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>
);
}Difference from useTransition:
useTransition: You control when to defer (wrap updates)useDeferredValue: React controls when to defer (wrap values)
Concurrent Rendering Benefits
Interruptible Rendering
- React can pause expensive work
- Prioritizes user interactions (clicks, typing)
- Resumes work when browser is idle
Automatic Prioritization
- Urgent updates (user input) render immediately
- Non-urgent updates (filtering, sorting) can wait
- Smoother user experience without manual debouncing
Better Loading States
function App() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
const handleTabChange = (newTab) => {
startTransition(() => {
setTab(newTab);
});
};
return (
<div>
<Tabs
activeTab={tab}
onChange={handleTabChange}
isPending={isPending}
/>
<TabContent tab={tab} />
</div>
);
}Migration Guide
Before React 18
// Manual debouncing for performance
const debouncedSearch = debounce((value) => {
setResults(searchItems(value));
}, 300);
<input onChange={(e) => debouncedSearch(e.target.value)} />With React 18
// Automatic prioritization with useTransition
const handleSearch = (value) => {
setQuery(value); // Immediate
startTransition(() => {
setResults(searchItems(value)); // Deferred
});
};
<input onChange={(e) => handleSearch(e.target.value)} />Performance Comparison
Without concurrent features:
- User types → UI freezes during expensive filter
- Perceived lag and unresponsiveness
- Manual debouncing required
With concurrent features:
- User types → Input updates immediately
- Filter runs in background
- UI stays responsive
- No manual optimization needed
Memoization Patterns
React.memo for Component Memoization
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:
- Component renders with same props frequently
- Expensive rendering logic (complex JSX, heavy computations)
- Child components in frequently updating parent
- List items with stable props
When NOT to use:
- Props change on every render (comparison overhead)
- Simple, fast-rendering components (unnecessary optimization)
useMemo for Expensive Computations
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:
- Expensive array operations (filter, map, sort, reduce)
- Complex mathematical calculations
- Data transformations and aggregations
- Creating derived data structures
Performance impact:
- Without useMemo: Computation runs every render
- With useMemo: Computation runs only when dependencies change
useCallback for Stable Function References
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:
- Use
useCallbackwhen passing functions to memoized child components - Without it, new function reference on every render defeats memo optimization
Profiling and Debugging
React DevTools Profiler
Identify and diagnose performance bottlenecks:
Programmatic Profiling
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
Step 1: Record a profile 1. Open React DevTools → Profiler tab 2. Click record button (red circle) 3. Interact with your app (click, type, navigate) 4. Click stop button
Step 2: Analyze flame graph
- Yellow/red bars = slow components
- Bar width = time spent rendering
- Click bar to see component details
- Look for unexpected renders
Step 3: Check ranked view
- Components sorted by render time
- Identify most expensive renders
- Focus optimization efforts here
Step 4: Investigate renders
- Why did this component render?
- Did props/state actually change?
- Is memo/useMemo working correctly?
Step 5: Compare profiles
- Record baseline profile
- Apply optimizations
- Record new profile
- Compare improvements
Browser Performance API
Custom performance measurements:
// Measure component render time
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`);
// Measure async operations
async function fetchData() {
performance.mark('fetch-start');
const data = await fetch('/api/data');
performance.mark('fetch-end');
performance.measure('data-fetch', 'fetch-start', 'fetch-end');
}Why Did You Render
Debug unnecessary re-renders in development:
npm install --save-dev @welldone-software/why-did-you-render// wdyr.js
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
trackHooks: true,
logOnDifferentValues: true,
});
}// Component.jsx
function MyComponent(props) {
// ...
}
MyComponent.whyDidYouRender = true;
export default MyComponent;Chrome DevTools Performance Tab
Identify non-React performance issues:
1. Open Chrome DevTools → Performance tab 2. Click record → Interact with app → Stop 3. Analyze timeline:
- Scripting (yellow): JavaScript execution
- Rendering (purple): Style calculations, layout
- Painting (green): Pixel painting
- System (gray): Browser overhead
Look for:
- Long tasks (>50ms) blocking the main thread
- Excessive layout recalculations
- Forced synchronous layout (layout thrashing)
- Large JavaScript bundles
Lighthouse Performance Audit
Automated performance analysis:
# Install Lighthouse CLI
npm install -g lighthouse
# Run audit
lighthouse https://your-app.com --view
# CI integration
lighthouse https://your-app.com --output json --output-path ./report.jsonKey metrics:
- First Contentful Paint (FCP): < 1.8s
- Largest Contentful Paint (LCP): < 2.5s
- Time to Interactive (TTI): < 3.8s
- Total Blocking Time (TBT): < 200ms
- Cumulative Layout Shift (CLS): < 0.1
Performance Monitoring in Production
Real User Monitoring (RUM):
import { useEffect } from 'react';
function App() {
useEffect(() => {
// Report Web Vitals
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
});
}, []);
return <div>...</div>;
}
function sendToAnalytics({ name, value, id }) {
// Send to analytics service (Google Analytics, Datadog, etc.)
gtag('event', name, {
event_category: 'Web Vitals',
value: Math.round(value),
event_label: id,
non_interaction: true,
});
}Common Performance Patterns to Look For
1. Unnecessary Re-renders
- Component renders but props/state unchanged
- Parent re-renders causing child cascade
- Missing memo on expensive child components
2. Expensive Computations
- Complex calculations on every render
- Missing useMemo for derived data
- Sorting/filtering large arrays without memoization
3. Memory Leaks
- Event listeners not cleaned up
- Timers not cleared
- Subscriptions not unsubscribed
4. Bundle Size Issues
- Large dependencies imported unnecessarily
- Missing code splitting
- Duplicate dependencies in bundle
State Management for Performance
State Structure Optimization
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
}Context Splitting
Prevent unnecessary context re-renders:
// 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
Choose the right tool for the job:
Local State First
// useState for component-level state
const [count, setCount] = useState(0);
// useReducer for complex state logic
const [state, dispatch] = useReducer(reducer, initialState);Context for Shared State
- Split by update frequency
- Avoid putting everything in one context
- Use memo to prevent unnecessary re-renders
External State Managers
Zustand (Recommended for simplicity)
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}Jotai (Atomic state management)
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Redux Toolkit (Complex apps)
- Use for large applications
- Time-travel debugging
- DevTools integration
- Middleware ecosystem
Server State Libraries
React Query (Recommended)
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5000,
});
if (isLoading) return <Spinner />;
return <div>{data.name}</div>;
}SWR (Alternative)
import useSWR from 'swr';
function UserProfile({ userId }) {
const { data, error } = useSWR(`/api/user/${userId}`, fetcher);
if (error) return <Error />;
if (!data) return <Spinner />;
return <div>{data.name}</div>;
}Avoiding Derived State
// 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
// GOOD: useMemo for expensive derivations
const expensiveValue = useMemo(() =>
items.reduce((sum, item) => sum + item.value, 0),
[items]
);Virtualization for Large Lists
react-window Basics
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 Lists
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
Traditional list with 10,000 items:
- DOM nodes: 10,000
- Memory usage: High
- Scroll performance: Poor
Virtualized list:
- DOM nodes: ~20 (only visible + buffer)
- Memory usage: Low
- Scroll performance: Smooth 60fps
- Result: 500x reduction in DOM nodes
Library Comparison
react-window (Recommended)
- Lightweight (7KB gzipped)
- Simple API
- Excellent performance
- Good for most use cases
react-virtualized
- Feature-rich (27KB gzipped)
- Advanced components (Grid, Masonry, Table)
- More configuration options
- Better for complex layouts
@tanstack/react-virtual
- Modern, headless virtualization
- Framework agnostic core
- Maximum flexibility
- Better for custom implementations
List Keys Optimization
Proper 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:
- React uses keys to track element identity
- Stable keys enable efficient diffing and reconciliation
- Index keys break when list order changes
- Missing keys force React to destroy/recreate components
# React Performance Optimization Skill Quality Rubric
version: "1.0.0"
skill_name: react-performance-optimization
evaluated_date: "2026-01-05"
dimensions:
clarity:
weight: 25
description: "Clear React performance concepts"
criteria:
- "Re-render causes are clearly explained"
- "Memoization strategies are visual"
- "Bundle optimization steps are clear"
- "Profiler usage is step-by-step"
completeness:
weight: 25
description: "Comprehensive React performance coverage"
criteria:
- "Covers React.memo, useMemo, useCallback"
- "Includes code splitting/lazy loading"
- "Has virtualization for large lists"
- "Covers state management perf"
accuracy:
weight: 30
description: "Correct React 18+ patterns"
criteria:
- "Follows React 18 best practices"
- "Concurrent features correctly used"
- "No deprecated patterns (componentWillMount)"
- "Hooks rules followed"
usefulness:
weight: 20
description: "Practical React optimization"
criteria:
- "Before/after performance comparisons"
- "Lighthouse metrics explained"
- "Real-world optimization examples"
- "Debugging slow components"
passing_criteria:
minimum_score: 3.5
target_score: 4.0
exceptional_score: 4.5
required_dimensions:
- accuracy
blocking_issues:
- "Uses deprecated lifecycle methods"
- "Incorrect hooks usage"
- "Anti-pattern examples without warning"
- "Class components without functional alternatives"
scoring_guide:
clarity:
"1": "Performance concepts jumbled"
"2": "Confusing optimization advice"
"3": "Understandable basics"
"4": "Clear with profiler examples"
"5": "Exceptional visual comparisons"
accuracy:
"1": "Outdated React patterns"
"2": "Class component focus"
"3": "Mostly modern patterns"
"4": "React 18+ best practices"
"5": "Cutting-edge, future-proof"