React & Next.js Topic
React Performance Optimization
1. React.memo - Preventing Unnecessary Re-renders
Basic Usage
jsx
import React, { memo, useState } from 'react';
// Without React.memo - re-renders when parent re-renders
const RegularComponent = ({ user, onClick }) => {
console.log('RegularComponent rendered');
return (
<div onClick={onClick}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
};
// With React.memo - only re-renders when props change
const OptimizedComponent = memo(({ user, onClick }) => {
console.log('OptimizedComponent rendered');
return (
<div onClick={onClick}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
// Custom comparison function
const DeepCompareComponent = memo(({ user, config }) => {
console.log('DeepCompareComponent rendered');
return (
<div>
<h3>{user.name}</h3>
<p>Theme: {config.theme}</p>
</div>
);
}, (prevProps, nextProps) => {
// Custom comparison - return true if props are equal (no re-render)
return (
prevProps.user.id === nextProps.user.id &&
prevProps.config.theme === nextProps.config.theme
);
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ id: 1, name: 'John', email: 'john@example.com' });
const [theme, setTheme] = useState('light');
const handleClick = () => {
console.log('Button clicked');
};
// This would cause unnecessary re-renders without useCallback
const stableHandleClick = React.useCallback(() => {
console.log('Button clicked');
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Re-render Parent ({count})
</button>
<button onClick={() => setUser({ ...user, name: 'John Updated' })}>
Update User
</button>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
{/* This re-renders every time parent re-renders */}
<RegularComponent user={user} onClick={handleClick} />
{/* This only re-renders when user prop changes */}
<OptimizedComponent user={user} onClick={stableHandleClick} />
{/* Custom comparison for complex props */}
<DeepCompareComponent
user={user}
config={{ theme }}
/>
</div>
);
}
When to Use React.memo
jsx
import React, { memo } from 'react';
// Good candidates for memo:
// - Components that render often
// - Components with expensive rendering
// - Pure functional components
// - Components that receive the same props frequently
const UserProfile = memo(({ user, onEdit }) => {
// Expensive calculation
const formattedUser = React.useMemo(() => {
console.log('Formatting user...');
return {
...user,
displayName: `${user.firstName} ${user.lastName}`.trim(),
joined: new Date(user.joinDate).toLocaleDateString()
};
}, [user]);
return (
<div className="user-profile">
<h2>{formattedUser.displayName}</h2>
<p>Joined: {formattedUser.joined}</p>
<button onClick={onEdit}>Edit</button>
</div>
);
});
// Avoid memo for:
// - Components that always receive different props
// - Very simple components
// - Components that need to re-render frequently anyway
const SimpleButton = ({ children, onClick }) => (
<button onClick={onClick}>{children}</button>
);
2. Lazy Loading + Suspense
Basic Route-based Code Splitting
jsx
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Lazy load pages
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
// Loading component
const LoadingFallback = () => (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
// Error boundary for lazy components
class LazyComponentErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Lazy component error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong while loading this page.</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
function App() {
return (
<Router>
<div className="app">
<LazyComponentErrorBoundary>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/dashboard/*" element={<Dashboard />} />
</Routes>
</Suspense>
</LazyComponentErrorBoundary>
</div>
</Router>
);
}
Component-level Lazy Loading
jsx
import React, { lazy, Suspense, useState } from 'react';
// Lazy load heavy components
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const DataGrid = lazy(() => import('./components/DataGrid'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
// Preload components
const preloadComponents = {
chart: () => import('./components/HeavyChart'),
grid: () => import('./components/DataGrid'),
editor: () => import('./components/RichTextEditor')
};
function AnalyticsDashboard() {
const [activeTab, setActiveTab] = useState('overview');
const [preloaded, setPreloaded] = useState(new Set());
const preloadComponent = (componentName) => {
if (!preloaded.has(componentName) && preloadComponents[componentName]) {
preloadComponents[componentName]();
setPreloaded(prev => new Set(prev).add(componentName));
}
};
const renderTabContent = () => {
switch (activeTab) {
case 'charts':
return (
<Suspense fallback={<ChartLoader />}>
<HeavyChart />
</Suspense>
);
case 'data':
return (
<Suspense fallback={<GridLoader />}>
<DataGrid />
</Suspense>
);
case 'editor':
return (
<Suspense fallback={<EditorLoader />}>
<RichTextEditor />
</Suspense>
);
default:
return <Overview />;
}
};
return (
<div className="dashboard">
<nav className="tabs">
<button
onClick={() => setActiveTab('overview')}
className={activeTab === 'overview' ? 'active' : ''}
>
Overview
</button>
<button
onClick={() => {
setActiveTab('charts');
preloadComponent('chart');
}}
onMouseEnter={() => preloadComponent('chart')}
className={activeTab === 'charts' ? 'active' : ''}
>
Charts
</button>
<button
onClick={() => {
setActiveTab('data');
preloadComponent('grid');
}}
onMouseEnter={() => preloadComponent('grid')}
className={activeTab === 'data' ? 'active' : ''}
>
Data Grid
</button>
<button
onClick={() => {
setActiveTab('editor');
preloadComponent('editor');
}}
onMouseEnter={() => preloadComponent('editor')}
className={activeTab === 'editor' ? 'active' : ''}
>
Editor
</button>
</nav>
<div className="tab-content">
{renderTabContent()}
</div>
</div>
);
}
// Custom loading components
const ChartLoader = () => (
<div className="loader">
<div className="chart-skeleton">
<div className="skeleton-bar"></div>
<div className="skeleton-bar"></div>
<div className="skeleton-bar"></div>
</div>
</div>
);
const GridLoader = () => (
<div className="loader">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="skeleton-row"></div>
))}
</div>
);
const EditorLoader = () => (
<div className="loader">
<div className="skeleton-toolbar"></div>
<div className="skeleton-editor"></div>
</div>
);
3. Avoiding Unnecessary Re-renders
Proper useCallback and useMemo
jsx
import React, { useState, useCallback, useMemo, memo } from 'react';
// Optimized parent component
function UserManagement() {
const [users, setUsers] = useState([]);
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState('name');
// Bad - recreates function on every render
const badAddUser = (user) => {
setUsers(prev => [...prev, user]);
};
// Good - stable function reference
const addUser = useCallback((user) => {
setUsers(prev => [...prev, user]);
}, []);
// Good - function with dependency
const removeUser = useCallback((userId) => {
setUsers(prev => prev.filter(user => user.id !== userId));
}, []);
// Bad - expensive calculation on every render
const badFilteredUsers = users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
).sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
// Good - memoized expensive calculation
const filteredUsers = useMemo(() => {
console.log('Filtering and sorting users...');
return users
.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
)
.sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
}, [users, filter, sortBy]);
// Good - memoized derived data
const userStats = useMemo(() => {
return {
total: users.length,
filtered: filteredUsers.length,
active: users.filter(user => user.active).length
};
}, [users, filteredUsers]);
return (
<div>
<SearchFilter
filter={filter}
onFilterChange={setFilter}
sortBy={sortBy}
onSortChange={setSortBy}
/>
<UserStats stats={userStats} />
<UserList
users={filteredUsers}
onAddUser={addUser}
onRemoveUser={removeUser}
/>
</div>
);
}
// Optimized child components
const SearchFilter = memo(({ filter, onFilterChange, sortBy, onSortChange }) => {
console.log('SearchFilter rendered');
return (
<div className="search-filter">
<input
type="text"
value={filter}
onChange={(e) => onFilterChange(e.target.value)}
placeholder="Filter users..."
/>
<select value={sortBy} onChange={(e) => onSortChange(e.target.value)}>
<option value="name">Name</option>
<option value="email">Email</option>
<option value="date">Date</option>
</select>
</div>
);
});
const UserStats = memo(({ stats }) => {
console.log('UserStats rendered');
return (
<div className="user-stats">
<div>Total: {stats.total}</div>
<div>Filtered: {stats.filtered}</div>
<div>Active: {stats.active}</div>
</div>
);
});
const UserList = memo(({ users, onAddUser, onRemoveUser }) => {
console.log('UserList rendered');
return (
<div className="user-list">
{users.map(user => (
<UserItem
key={user.id}
user={user}
onRemove={onRemoveUser}
/>
))}
</div>
);
});
const UserItem = memo(({ user, onRemove }) => {
console.log(`UserItem ${user.id} rendered`);
return (
<div className="user-item">
<span>{user.name} - {user.email}</span>
<button onClick={() => onRemove(user.id)}>Remove</button>
</div>
);
});
Context Optimization
jsx
import React, { createContext, useContext, useMemo, useState } from 'react';
// Bad context - causes unnecessary re-renders
const BadAppContext = createContext();
function BadAppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
// This value changes on every render
const value = {
user,
setUser,
theme,
setTheme,
notifications,
setNotifications
};
return (
<BadAppContext.Provider value={value}>
{children}
</BadAppContext.Provider>
);
}
// Optimized context
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
// Memoize the context value
const value = useMemo(() => ({
user,
setUser,
theme,
setTheme,
notifications,
setNotifications
}), [user, theme, notifications]);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
// Split contexts for better performance
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();
function SplitContextProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
return (
<UserContext.Provider value={useMemo(() => [user, setUser], [user])}>
<ThemeContext.Provider value={useMemo(() => [theme, setTheme], [theme])}>
<NotificationContext.Provider value={useMemo(() => [notifications, setNotifications], [notifications])}>
{children}
</NotificationContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// Optimized consumers
function UserProfile() {
const [user] = useContext(UserContext);
// Only re-renders when user changes
return <div>Welcome, {user?.name}</div>;
}
function ThemeToggle() {
const [theme, setTheme] = useContext(ThemeContext);
// Only re-renders when theme changes
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}
4. Virtualization with react-window
Basic Virtualized List
jsx
import React from 'react';
import { FixedSizeList as List } from 'react-window';
// Without virtualization - renders ALL items
function RegularList({ items }) {
return (
<div className="list" style={{ height: '400px', overflow: 'auto' }}>
{items.map(item => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}
// With virtualization - renders only visible items
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
<ListItem item={items[index]} />
</div>
);
return (
<List
height={400}
itemCount={items.length}
itemSize={80} // Height of each row
width="100%"
>
{Row}
</List>
);
}
const ListItem = memo(({ item }) => {
return (
<div className="list-item">
<img src={item.avatar} alt={item.name} width="50" height="50" />
<div>
<h3>{item.name}</h3>
<p>{item.email}</p>
<p>{item.description}</p>
</div>
</div>
);
});
Advanced Virtualization Examples
jsx
import React, { useMemo } from 'react';
import { FixedSizeList as List, VariableSizeList } from 'react-window';
// Variable size list for dynamic content
function VariableSizeVirtualizedList({ items }) {
const getItemSize = index => {
const item = items[index];
// Calculate height based on content
const baseHeight = 80;
const descriptionHeight = Math.ceil(item.description.length / 100) * 20;
return baseHeight + descriptionHeight;
};
const Row = ({ index, style }) => (
<div style={style}>
<VariableListItem item={items[index]} />
</div>
);
return (
<VariableSizeList
height={400}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
// Virtualized grid
function VirtualizedGrid({ items, columns = 4 }) {
const rows = useMemo(() => {
const result = [];
for (let i = 0; i < items.length; i += columns) {
result.push(items.slice(i, i + columns));
}
return result;
}, [items, columns]);
const Row = ({ index, style }) => (
<div style={style} className="grid-row">
{rows[index].map(item => (
<GridItem key={item.id} item={item} />
))}
</div>
);
return (
<List
height={400}
itemCount={rows.length}
itemSize={120} // Height of each grid row
width="100%"
>
{Row}
</List>
);
}
// Virtualized table with headers
function VirtualizedTable({ columns, data }) {
const Row = ({ index, style }) => {
const row = data[index];
return (
<div style={style} className="table-row">
{columns.map(column => (
<div key={column.key} className="table-cell">
{row[column.key]}
</div>
))}
</div>
);
};
return (
<div className="virtualized-table">
{/* Fixed header */}
<div className="table-header">
{columns.map(column => (
<div key={column.key} className="header-cell">
{column.title}
</div>
))}
</div>
{/* Virtualized body */}
<List
height={300}
itemCount={data.length}
itemSize={40}
width="100%"
>
{Row}
</List>
</div>
);
}
// Infinite loading virtualized list
function InfiniteVirtualizedList({ items, hasMore, loadMore, loading }) {
const itemCount = hasMore ? items.length + 1 : items.length;
const Row = ({ index, style }) => {
if (index === items.length) {
return (
<div style={style} className="loading-row">
{loading ? 'Loading more...' : 'No more items'}
</div>
);
}
return (
<div style={style}>
<ListItem item={items[index]} />
</div>
);
};
const loadMoreItems = () => {
if (hasMore && !loading) {
loadMore();
}
};
return (
<List
height={400}
itemCount={itemCount}
itemSize={80}
width="100%"
onItemsRendered={({ visibleStopIndex }) => {
if (visibleStopIndex >= items.length - 5) {
loadMoreItems();
}
}}
>
{Row}
</List>
);
}
Performance Monitoring and Debugging
jsx
import React, { Profiler } from 'react';
// Performance monitoring component
function PerformanceMonitor() {
const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
console.log({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
});
// Log slow renders
if (actualDuration > 16) { // 16ms = 60fps
console.warn(`Slow render detected in ${id}: ${actualDuration}ms`);
}
};
return (
<Profiler id="App" onRender={onRender}>
<App />
</Profiler>
);
}
// Custom hook for performance tracking
function useRenderTracking(componentName) {
const renderCount = React.useRef(0);
React.useEffect(() => {
renderCount.current += 1;
console.log(`${componentName} rendered: ${renderCount.current} times`);
});
}
// Why did you render (for development)
// Add this in your components during development:
/*
MyComponent.whyDidYouRender = {
logOnDifferentValues: true,
customName: 'MyComponent'
};
*/
Performance Optimization Checklist
Do:
-
Use React.memo for expensive components
-
Use useCallback for function props
-
Use useMemo for expensive calculations
-
Implement code splitting with lazy loading
-
Virtualize long lists
-
Optimize context providers
-
Use proper dependency arrays
Don't:
-
Over-optimize simple components
-
Use memo for components that change often
-
Create new objects/functions in render without useMemo/useCallback
-
Ignore the bundle size impact
-
Virtualize very short lists
This comprehensive performance optimization guide covers the most effective strategies for making your React applications faster and more efficient.
Comments (0)
No comments yet. Be the first to comment!