Web Development / React / Performance / Optimization / JavaScript
React Performance Optimization: Advanced Techniques
Master advanced React performance optimization techniques including memoization, code splitting, and virtual scrolling for lightning-fast applications.
On this page
- Understanding React's Rendering Process
- Profiling Your Application
- Memoization Techniques
- React.memo
- useMemo Hook
- useCallback Hook
- Code Splitting and Lazy Loading
- Component-Level Code Splitting
- Route-Level Code Splitting
- Virtual Scrolling for Large Lists
- State Management Optimization
- Minimize State Updates
- Use Reducers for Complex State
- Bundle Analysis and Optimization
- Analyze Your Bundle
- Tree Shaking
- Performance Monitoring
- Web Vitals
- Conclusion
Performance is crucial for modern web applications. Users expect fast, responsive interfaces, and React provides several powerful tools to help us achieve optimal performance. In this comprehensive guide, we'll explore advanced techniques to make your React applications lightning-fast.
Understanding React's Rendering Process
Before diving into optimization techniques, it's essential to understand how React renders components:
- Trigger: Something causes a re-render (state change, props change, parent re-render)
- Render: React calls your component functions to determine what should be displayed
- Commit: React applies changes to the DOM
Profiling Your Application
Always measure before optimizing. React DevTools Profiler helps identify performance bottlenecks:
// Enable profiling in development
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log('Component:', id, 'Phase:', phase, 'Duration:', actualDuration);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
}Memoization Techniques
React.memo
Prevent unnecessary re-renders of functional components:
import React, { memo } from 'react';
interface UserCardProps {
user: {
id: number;
name: string;
email: string;
};
onEdit: (id: number) => void;
}
const UserCard = memo(({ user, onEdit }: UserCardProps) => {
console.log('UserCard rendered for:', user.name);
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
});
// Custom comparison function
const UserCardOptimized = memo(
({ user, onEdit }: UserCardProps) => {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
},
(prevProps, nextProps) => {
// Only re-render if user data actually changed
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email
);
}
);useMemo Hook
Memoize expensive calculations:
import { useMemo, useState } from 'react';
function ExpensiveComponent({ items }: { items: number[] }) {
const [filter, setFilter] = useState('');
// Expensive calculation - only runs when items or filter changes
const filteredAndSortedItems = useMemo(() => {
console.log('Calculating filtered items...');
return items
.filter(item => item.toString().includes(filter))
.sort((a, b) => b - a);
}, [items, filter]);
// Another expensive calculation
const statistics = useMemo(() => {
console.log('Calculating statistics...');
return {
total: filteredAndSortedItems.length,
sum: filteredAndSortedItems.reduce((acc, item) => acc + item, 0),
average:
filteredAndSortedItems.length > 0
? filteredAndSortedItems.reduce((acc, item) => acc + item, 0) /
filteredAndSortedItems.length
: 0,
};
}, [filteredAndSortedItems]);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<div>Total: {statistics.total}</div>
<div>Sum: {statistics.sum}</div>
<div>Average: {statistics.average.toFixed(2)}</div>
<ul>
{filteredAndSortedItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}useCallback Hook
Memoize function references to prevent child re-renders:
import { useCallback, useState, memo } from 'react';
// Child component that should only re-render when necessary
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log('TodoItem rendered:', todo.text);
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});
function TodoList() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
// Without useCallback, these functions are recreated on every render
// causing TodoItem to re-render even when todo data hasn't changed
const handleToggle = useCallback((id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const handleDelete = useCallback((id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
const addTodo = useCallback(() => {
if (newTodo.trim()) {
setTodos(prev => [
...prev,
{
id: Date.now(),
text: newTodo,
completed: false,
},
]);
setNewTodo('');
}
}, [newTodo]);
return (
<div>
<input
value={newTodo}
onChange={e => setNewTodo(e.target.value)}
placeholder="Add new todo..."
/>
<button onClick={addTodo}>Add</button>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
);
}Code Splitting and Lazy Loading
Component-Level Code Splitting
import { lazy, Suspense } from 'react';
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
function App() {
const [currentView, setCurrentView] = useState('dashboard');
const renderView = () => {
switch (currentView) {
case 'dashboard':
return <Dashboard />;
case 'profile':
return <Profile />;
case 'settings':
return <Settings />;
default:
return <Dashboard />;
}
};
return (
<div>
<nav>
<button onClick={() => setCurrentView('dashboard')}>Dashboard</button>
<button onClick={() => setCurrentView('profile')}>Profile</button>
<button onClick={() => setCurrentView('settings')}>Settings</button>
</nav>
<Suspense fallback={<div>Loading...</div>}>{renderView()}</Suspense>
</div>
);
}Route-Level Code Splitting
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div className="loading">Loading page...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}Virtual Scrolling for Large Lists
For lists with thousands of items, virtual scrolling only renders visible items:
import { FixedSizeList as List } from 'react-window';
interface ItemData {
id: number;
name: string;
description: string;
}
const Row = ({
index,
style,
data,
}: {
index: number;
style: React.CSSProperties;
data: ItemData[];
}) => (
<div style={style} className="list-item">
<h4>{data[index].name}</h4>
<p>{data[index].description}</p>
</div>
);
function VirtualizedList({ items }: { items: ItemData[] }) {
return (
<List
height={600} // Container height
itemCount={items.length}
itemSize={100} // Height of each item
itemData={items}
width="100%"
>
{Row}
</List>
);
}State Management Optimization
Minimize State Updates
// Bad: Multiple state updates
function BadComponent() {
const [count, setCount] = useState(0);
const [doubled, setDoubled] = useState(0);
const [tripled, setTripled] = useState(0);
const increment = () => {
setCount(c => c + 1);
setDoubled(c => (c + 1) * 2); // Causes extra re-render
setTripled(c => (c + 1) * 3); // Causes extra re-render
};
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Tripled: {tripled}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// Good: Derived state
function GoodComponent() {
const [count, setCount] = useState(0);
// Derived values - no extra state needed
const doubled = count * 2;
const tripled = count * 3;
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Tripled: {tripled}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}Use Reducers for Complex State
import { useReducer } from 'react';
interface State {
items: Item[];
loading: boolean;
error: string | null;
filter: string;
}
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: Item[] }
| { type: 'FETCH_ERROR'; payload: string }
| { type: 'SET_FILTER'; payload: string }
| { type: 'ADD_ITEM'; payload: Item }
| { type: 'DELETE_ITEM'; payload: number };
function itemsReducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, items: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'SET_FILTER':
return { ...state, filter: action.payload };
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'DELETE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
};
default:
return state;
}
}Bundle Analysis and Optimization
Analyze Your Bundle
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Add to package.json scripts
"analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"
# Run analysis
npm run analyzeTree Shaking
Import only what you need:
// Bad: Imports entire library
import * as _ from 'lodash';
// Good: Import specific functions
import { debounce, throttle } from 'lodash';
// Even better: Use individual packages
import debounce from 'lodash.debounce';Performance Monitoring
Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// Send to your analytics service
console.log(metric);
}
// Measure all Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);Conclusion
React performance optimization is an ongoing process that requires careful measurement and strategic implementation. Key takeaways:
- Profile first: Always measure before optimizing
- Memoize wisely: Use React.memo, useMemo, and useCallback strategically
- Split your code: Implement lazy loading for better initial load times
- Optimize state: Minimize state updates and use derived state when possible
- Monitor continuously: Track performance metrics in production
Remember, premature optimization can lead to complex code without significant benefits. Focus on the bottlenecks that actually impact user experience, and always test your optimizations to ensure they provide real improvements.
By implementing these techniques thoughtfully, you'll create React applications that are not only fast but also maintainable and scalable.