Welcome to Performance Optimization!
Imagine you're a chef in a busy restaurant. You don't want to remake the same dish from scratch every time it's ordered - you prepare some components in advance and reuse them. Similarly, React.memo and useMemo help us avoid unnecessary work by remembering previous results. Today, we'll master the art of optimizing React applications!
graph TD
A[Component Render] --> B{Props Changed?}
B -->|Yes| C[Re-render Component]
B -->|No| D[Use Memoized Version]
C --> E[Execute Expensive Calculations]
D --> F[Skip Calculations]
E --> G[Update UI]
F --> G
style A fill:#ff9999
style D fill:#99ff99
style F fill:#99ff99
Understanding React.memo
1. Basic React.memo Usage
// Without React.memo - re-renders on every parent update
function ExpensiveComponent({ data }) {
console.log('ExpensiveComponent rendered');
// Simulate expensive operation
const processedData = data.map(item => {
// Complex calculations
return item * 2;
});
return (
<div>
<h3>Processed Data</h3>
{processedData.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
}
// With React.memo - only re-renders when props change
const MemoizedComponent = React.memo(function ExpensiveComponent({ data }) {
console.log('MemoizedComponent rendered');
const processedData = data.map(item => item * 2);
return (
<div>
<h3>Processed Data</h3>
{processedData.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
});
// Parent component
function Parent() {
const [count, setCount] = useState(0);
const [data, setData] = useState([1, 2, 3, 4, 5]);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Increment Count: {count}
</button>
{/* This will re-render on every count change */}
<ExpensiveComponent data={data} />
{/* This will only re-render when data changes */}
<MemoizedComponent data={data} />
</div>
);
}
2. Custom Comparison Function
// React.memo with custom comparison
const MemoizedComponent = React.memo(
function UserCard({ user }) {
console.log('UserCard rendered for:', user.name);
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>Last Active: {user.lastActive}</p>
</div>
);
},
// Custom comparison function
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
// Return false if props are different (re-render)
// Only re-render if name or email changes
// Ignore lastActive changes
return (
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email
);
}
);
// Advanced comparison for complex objects
const ComplexMemoizedComponent = React.memo(
function DataGrid({ data, config, onItemClick }) {
return (
<div className="data-grid">
{data.map(item => (
<div
key={item.id}
onClick={() => onItemClick(item)}
className={`grid-item ${config.highlight === item.id ? 'highlighted' : ''}`}
>
{item.name}
</div>
))}
</div>
);
},
(prevProps, nextProps) => {
// Deep comparison for data array
const dataEqual = JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data);
// Shallow comparison for config
const configEqual = prevProps.config.highlight === nextProps.config.highlight;
// Reference comparison for callback
const callbackEqual = prevProps.onItemClick === nextProps.onItemClick;
return dataEqual && configEqual && callbackEqual;
}
);
// Helper function for deep comparison
function arePropsEqual(prevProps, nextProps) {
// Custom deep equality check
return Object.keys(prevProps).every(key => {
if (typeof prevProps[key] === 'function') {
// Functions must be the same reference
return prevProps[key] === nextProps[key];
}
if (typeof prevProps[key] === 'object') {
// Deep compare objects
return JSON.stringify(prevProps[key]) === JSON.stringify(nextProps[key]);
}
// Primitive values
return prevProps[key] === nextProps[key];
});
}
const SmartMemoizedComponent = React.memo(MyComponent, arePropsEqual);
3. When to Use React.memo
// Good use case: Expensive rendering component
const ExpensiveChart = React.memo(function Chart({ data, options }) {
// Complex chart rendering logic
const chartInstance = useRef(null);
useEffect(() => {
if (!chartInstance.current) {
chartInstance.current = new ChartLibrary(canvasRef.current);
}
chartInstance.current.update(data, options);
}, [data, options]);
return <canvas ref={canvasRef} />;
});
// Good use case: Frequently re-rendered parent
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
const [notifications, setNotifications] = useState([]);
// This component re-renders often due to notifications
useEffect(() => {
const ws = new WebSocket('ws://api/notifications');
ws.onmessage = (event) => {
setNotifications(prev => [...prev, event.data]);
};
return () => ws.close();
}, []);
return (
<div>
<TabBar activeTab={activeTab} onTabChange={setActiveTab} />
{/* Memoize children to prevent unnecessary re-renders */}
<MemoizedSidebar />
<MemoizedMainContent activeTab={activeTab} />
{/* This updates frequently but shouldn't affect other components */}
<NotificationBadge count={notifications.length} />
</div>
);
}
// Bad use case: Simple component with few props
// Don't over-optimize!
const SimpleButton = React.memo(function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}); // Probably unnecessary - the overhead might be more than the benefit
Understanding useMemo
1. Basic useMemo Usage
// Without useMemo - recalculates on every render
function FilteredList({ items, filter }) {
console.log('Filtering items...');
// This runs on every render, even if items and filter haven't changed
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// With useMemo - only recalculates when dependencies change
function OptimizedFilteredList({ items, filter }) {
// Only recalculate when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
2. Complex Calculations with useMemo
function DataAnalytics({ data, config }) {
// Expensive calculation #1: Statistical analysis
const statistics = useMemo(() => {
console.log('Calculating statistics...');
return {
mean: data.reduce((sum, val) => sum + val, 0) / data.length,
median: calculateMedian(data),
mode: calculateMode(data),
standardDeviation: calculateStandardDeviation(data),
percentiles: calculatePercentiles(data, [25, 50, 75, 90, 95, 99])
};
}, [data]);
// Expensive calculation #2: Data transformation
const transformedData = useMemo(() => {
console.log('Transforming data...');
return data.map(value => ({
original: value,
normalized: (value - statistics.mean) / statistics.standardDeviation,
percentile: calculatePercentileRank(value, data),
category: categorizeValue(value, config.thresholds)
}));
}, [data, statistics, config.thresholds]);
// Expensive calculation #3: Visualization data
const chartData = useMemo(() => {
console.log('Preparing chart data...');
const bins = createHistogramBins(data, config.binCount);
const distribution = calculateDistribution(data, bins);
return {
histogram: bins.map((bin, i) => ({
x: bin.start,
y: distribution[i]
})),
boxPlot: {
min: Math.min(...data),
q1: statistics.percentiles[0],
median: statistics.median,
q3: statistics.percentiles[2],
max: Math.max(...data)
},
densityCurve: calculateKernelDensity(data, config.bandwidth)
};
}, [data, config.binCount, config.bandwidth, statistics]);
return (
<div className="analytics-dashboard">
<StatisticsSummary stats={statistics} />
<DataTable data={transformedData} />
<VisualizationPanel chartData={chartData} />
</div>
);
}
3. Memoizing Object and Array Values
function SearchComponent({ defaultFilters }) {
const [searchTerm, setSearchTerm] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
// Without useMemo: creates new object on every render
// This causes child components to re-render unnecessarily
const badFilterConfig = {
term: searchTerm,
order: sortOrder,
...defaultFilters
};
// With useMemo: only creates new object when dependencies change
const filterConfig = useMemo(() => ({
term: searchTerm,
order: sortOrder,
...defaultFilters
}), [searchTerm, sortOrder, defaultFilters]);
// Memoizing array transformations
const sortOptions = useMemo(() => [
{ value: 'asc', label: 'Ascending' },
{ value: 'desc', label: 'Descending' },
{ value: 'alpha', label: 'Alphabetical' },
{ value: 'date', label: 'By Date' }
], []); // Empty deps array - this never changes
// Memoizing callback creation
const handleSearch = useMemo(() => {
return debounce((term) => {
// API call logic here
console.log('Searching for:', term);
}, 300);
}, []); // Create debounced function only once
return (
<div>
<SearchInput
value={searchTerm}
onChange={setSearchTerm}
onSearch={handleSearch}
/>
<SortSelector
options={sortOptions}
value={sortOrder}
onChange={setSortOrder}
/>
<FilteredResults config={filterConfig} />
</div>
);
}
Combining React.memo and useMemo
// Parent component with expensive calculations
function DataDashboard({ rawData, filters }) {
const [sortConfig, setSortConfig] = useState({ key: 'name', order: 'asc' });
const [selectedItems, setSelectedItems] = useState(new Set());
// Expensive data processing
const processedData = useMemo(() => {
console.log('Processing data...');
return rawData
.filter(item => applyFilters(item, filters))
.map(item => enrichData(item))
.sort((a, b) => sortData(a, b, sortConfig));
}, [rawData, filters, sortConfig]);
// Expensive statistics calculation
const statistics = useMemo(() => {
console.log('Calculating statistics...');
return {
total: processedData.length,
averageValue: calculateAverage(processedData),
distribution: calculateDistribution(processedData)
};
}, [processedData]);
// Memoize callbacks to prevent child re-renders
const handleSort = useCallback((key) => {
setSortConfig(prev => ({
key,
order: prev.key === key && prev.order === 'asc' ? 'desc' : 'asc'
}));
}, []);
const handleSelect = useCallback((itemId) => {
setSelectedItems(prev => {
const next = new Set(prev);
if (next.has(itemId)) {
next.delete(itemId);
} else {
next.add(itemId);
}
return next;
});
}, []);
return (
<div>
<Statistics data={statistics} />
<DataTable
data={processedData}
onSort={handleSort}
sortConfig={sortConfig}
selectedItems={selectedItems}
onSelect={handleSelect}
/>
</div>
);
}
// Child component optimized with React.memo
const Statistics = React.memo(function Statistics({ data }) {
console.log('Statistics rendered');
return (
<div className="statistics">
<div>Total Items: {data.total}</div>
<div>Average: {data.averageValue.toFixed(2)}</div>
<DistributionChart data={data.distribution} />
</div>
);
});
// Another child component with internal memoization
const DataTable = React.memo(function DataTable({
data,
onSort,
sortConfig,
selectedItems,
onSelect
}) {
console.log('DataTable rendered');
// Memoize column definitions
const columns = useMemo(() => [
{
key: 'select',
header: '',
render: (item) => (
<input
type="checkbox"
checked={selectedItems.has(item.id)}
onChange={() => onSelect(item.id)}
/>
)
},
{
key: 'name',
header: 'Name',
sortable: true
},
{
key: 'value',
header: 'Value',
sortable: true,
render: (item) => item.value.toFixed(2)
}
], [selectedItems, onSelect]);
// Memoize header rendering
const headers = useMemo(() => (
<thead>
<tr>
{columns.map(column => (
<th
key={column.key}
onClick={() => column.sortable && onSort(column.key)}
className={sortConfig.key === column.key ? sortConfig.order : ''}
>
{column.header}
{column.sortable && sortConfig.key === column.key && (
<span>{sortConfig.order === 'asc' ? '↑' : '↓'}</span>
)}
</th>
))}
</tr>
</thead>
), [columns, sortConfig, onSort]);
return (
<table>
{headers}
<tbody>
{data.map(item => (
<tr key={item.id}>
{columns.map(column => (
<td key={column.key}>
{column.render ? column.render(item) : item[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
});
Common Performance Patterns
1. Memoizing Expensive Filters
function ProductList({ products, filters }) {
// Memoize filtered products
const filteredProducts = useMemo(() => {
return products.filter(product => {
// Price filter
if (filters.minPrice && product.price < filters.minPrice) return false;
if (filters.maxPrice && product.price > filters.maxPrice) return false;
// Category filter
if (filters.category && product.category !== filters.category) return false;
// Search filter
if (filters.search) {
const searchLower = filters.search.toLowerCase();
const inName = product.name.toLowerCase().includes(searchLower);
const inDescription = product.description.toLowerCase().includes(searchLower);
if (!inName && !inDescription) return false;
}
// Rating filter
if (filters.minRating && product.rating < filters.minRating) return false;
return true;
});
}, [products, filters]);
// Memoize sorted products
const sortedProducts = useMemo(() => {
const sorted = [...filteredProducts];
switch (filters.sortBy) {
case 'price-low':
return sorted.sort((a, b) => a.price - b.price);
case 'price-high':
return sorted.sort((a, b) => b.price - a.price);
case 'rating':
return sorted.sort((a, b) => b.rating - a.rating);
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
default:
return sorted;
}
}, [filteredProducts, filters.sortBy]);
// Memoize pagination
const paginatedProducts = useMemo(() => {
const start = (filters.page - 1) * filters.perPage;
const end = start + filters.perPage;
return sortedProducts.slice(start, end);
}, [sortedProducts, filters.page, filters.perPage]);
return (
<div>
<div>Showing {filteredProducts.length} products</div>
<ProductGrid products={paginatedProducts} />
<Pagination
total={filteredProducts.length}
perPage={filters.perPage}
currentPage={filters.page}
/>
</div>
);
}
2. Memoizing Context Values
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const [accentColor, setAccentColor] = useState('#007bff');
// Without memoization: creates new object every render
// This causes all consumers to re-render
const badContextValue = {
theme,
accentColor,
setTheme,
setAccentColor,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
};
// With memoization: only updates when dependencies change
const contextValue = useMemo(() => ({
theme,
accentColor,
setTheme,
setAccentColor,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
}), [theme, accentColor]);
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
}
// Advanced: Split context for better performance
function AppProviders({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [language, setLanguage] = useState('en');
// Separate contexts for different concerns
const authValue = useMemo(() => ({ user, setUser }), [user]);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
const i18nValue = useMemo(() => ({ language, setLanguage }), [language]);
return (
<AuthContext.Provider value={authValue}>
<ThemeContext.Provider value={themeValue}>
<I18nContext.Provider value={i18nValue}>
{children}
</I18nContext.Provider>
</ThemeContext.Provider>
</AuthContext.Provider>
);
}
3. Memoizing Heavy Computations
function ChartComponent({ data, config }) {
// Expensive data processing for visualization
const chartData = useMemo(() => {
console.log('Processing chart data...');
// Step 1: Clean and validate data
const cleanedData = data.filter(item =>
item.value !== null && !isNaN(item.value)
);
// Step 2: Calculate aggregates
const aggregates = cleanedData.reduce((acc, item) => {
const key = item[config.groupBy];
if (!acc[key]) {
acc[key] = { sum: 0, count: 0, values: [] };
}
acc[key].sum += item.value;
acc[key].count += 1;
acc[key].values.push(item.value);
return acc;
}, {});
// Step 3: Calculate statistics for each group
const statistics = Object.entries(aggregates).map(([key, group]) => ({
key,
average: group.sum / group.count,
median: calculateMedian(group.values),
min: Math.min(...group.values),
max: Math.max(...group.values),
count: group.count
}));
// Step 4: Format for chart library
return {
labels: statistics.map(stat => stat.key),
datasets: [{
label: 'Average',
data: statistics.map(stat => stat.average)
}, {
label: 'Median',
data: statistics.map(stat => stat.median)
}],
metadata: statistics
};
}, [data, config.groupBy]);
// Memoize chart options
const chartOptions = useMemo(() => ({
responsive: true,
plugins: {
legend: {
position: config.legendPosition || 'top',
},
title: {
display: true,
text: config.title || 'Data Visualization'
}
},
scales: {
y: {
beginAtZero: config.beginAtZero !== false
}
}
}), [config]);
return (
<div className="chart-container">
<Chart
type={config.chartType || 'bar'}
data={chartData}
options={chartOptions}
/>
<DataSummary statistics={chartData.metadata} />
</div>
);
}
Common Pitfalls and Solutions
1. Incorrect Dependencies
// WRONG: Missing dependency
function BadExample({ items }) {
const [filter, setFilter] = useState('');
// Missing 'filter' in dependency array!
const filtered = useMemo(() => {
return items.filter(item => item.includes(filter));
}, [items]); // Should be [items, filter]
return <List items={filtered} />;
}
// CORRECT: All dependencies included
function GoodExample({ items }) {
const [filter, setFilter] = useState('');
const filtered = useMemo(() => {
return items.filter(item => item.includes(filter));
}, [items, filter]);
return <List items={filtered} />;
}
// Using ESLint to catch dependency issues
// Install eslint-plugin-react-hooks
// Add to .eslintrc:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
2. Over-optimization
// OVER-OPTIMIZED: Simple calculations don't need memoization
function OverOptimized({ count }) {
// This is unnecessary - the calculation is trivial
const doubled = useMemo(() => count * 2, [count]);
// This is also unnecessary for a simple component
const button = useMemo(() => (
<button>Click me</button>
), []);
return <div>{doubled} {button}</div>;
}
// BETTER: Only optimize what's necessary
function Balanced({ items, complexFilter }) {
// Only memoize expensive operations
const filtered = useMemo(() => {
return items.filter(item => complexFilter.test(item));
}, [items, complexFilter]);
// Simple calculations can be inline
const count = filtered.length;
const message = `Found ${count} items`;
return <div>{message}</div>;
}
3. Reference Equality Issues
// PROBLEM: Objects/arrays break memoization
function Parent() {
const [count, setCount] = useState(0);
// This creates a new object on every render!
const config = { color: 'blue', size: 'large' };
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{/* Child will re-render every time! */}
<MemoizedChild config={config} />
</div>
);
}
// SOLUTION: Memoize object creation
function ParentFixed() {
const [count, setCount] = useState(0);
// Only create object once
const config = useMemo(() => ({
color: 'blue',
size: 'large'
}), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoizedChild config={config} />
</div>
);
}
// Also applies to callbacks
function ParentWithCallback() {
const [count, setCount] = useState(0);
// This creates a new function on every render!
const handleClick = () => {
console.log('clicked');
};
// Use useCallback instead
const handleClickMemoized = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoizedChild onClick={handleClickMemoized} />
</div>
);
}
Performance Debugging
1. Using React DevTools Profiler
// Add profiler to measure performance
import { Profiler } from 'react';
function onRenderCallback(
id, // the "id" prop of the Profiler tree that has just committed
phase, // either "mount" (if the tree just mounted) or "update"
actualDuration, // time spent rendering the committed update
baseDuration, // estimated time to render the entire subtree without memoization
startTime, // when React began rendering this update
commitTime, // when React committed this update
interactions // the Set of interactions belonging to this update
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MainContent />
</Profiler>
);
}
2. Custom Performance Hooks
// Hook to measure render count
function useRenderCount(componentName) {
const renderCount = useRef(0);
renderCount.current += 1;
useEffect(() => {
console.log(`${componentName} rendered ${renderCount.current} times`);
});
}
// Hook to measure computation time
function useComputationTime(label) {
const startTime = useRef(performance.now());
useEffect(() => {
const endTime = performance.now();
const duration = endTime - startTime.current;
console.log(`${label} took ${duration.toFixed(2)}ms`);
startTime.current = performance.now();
});
}
// Hook to detect unnecessary re-renders
function useWhyDidYouUpdate(name, props) {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps = {};
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object.keys(changedProps).length) {
console.log(`[why-did-you-update] ${name}:`, changedProps);
}
}
previousProps.current = props;
});
}
Best Practices
1. Measure Before Optimizing
// Use React DevTools Profiler first
// Only optimize components that actually need it
function MaybeOptimize({ data }) {
// First, check if this component is a performance bottleneck
// Use React DevTools to measure render time
// If it's slow, then optimize:
const processedData = useMemo(() => {
return expensiveOperation(data);
}, [data]);
return <DisplayData data={processedData} />;
}
2. Start with React.memo
// Often React.memo is enough
const ExpensiveComponent = React.memo(function({ data }) {
// Component logic here
});
// Add useMemo only if needed
const OptimizedComponent = React.memo(function({ data }) {
const processed = useMemo(() => {
return expensiveCalculation(data);
}, [data]);
return <Display data={processed} />;
});
3. Keep Dependencies Stable
// Move static values outside component
const STATIC_CONFIG = {
threshold: 100,
maxItems: 50
};
function Component({ items }) {
// This is stable across renders
const filtered = useMemo(() => {
return items.filter(item => item.value > STATIC_CONFIG.threshold);
}, [items]); // STATIC_CONFIG doesn't need to be a dependency
return <List items={filtered} />;
}
Practice Exercises
Exercise 1: Optimize a Data Table
Create a data table component that:
- Displays 1000+ rows of data
- Supports sorting by multiple columns
- Has filtering capabilities
- Uses virtualization for rendering
- Implements proper memoization
Exercise 2: Build a Chart Dashboard
Create a dashboard with multiple charts:
- Multiple chart types (bar, line, pie)
- Real-time data updates
- Expensive data processing
- Proper memoization of calculations
- Optimize re-renders when data changes
Exercise 3: Optimize a Form
Create a complex form with:
- Multiple sections that can be expanded/collapsed
- Dynamic field validation
- Conditional fields based on other values
- Autosave functionality
- Prevent unnecessary re-renders
Key Takeaways
- React.memo prevents unnecessary re-renders of components
- useMemo memoizes expensive calculations
- Always measure performance before optimizing
- Don't over-optimize - it can make code harder to maintain
- Keep dependency arrays accurate and complete
- Consider reference equality when passing objects/arrays
- Use React DevTools Profiler to identify bottlenecks