Back to articles
React

Advanced React Performance Optimization Techniques

Deep dive into React performance optimization including memoization, code splitting, lazy loading, and profiling. Real-world examples and performance measurement strategies.

10 mins read
ReactPerformanceOptimizationProfiling

React applications can become sluggish as they grow in complexity. Performance optimization is crucial for maintaining a smooth user experience. In this comprehensive guide, we'll explore advanced techniques to optimize React applications using memoization, code splitting, lazy loading, and profiling tools.

Understanding React Performance Bottlenecks

Before optimizing, it's essential to understand what causes performance issues in React applications:

  • Unnecessary re-renders of components
  • Heavy computations blocking the main thread
  • Large bundle sizes affecting initial load time
  • Memory leaks from uncleared intervals or subscriptions
  • Inefficient list rendering without proper keys

React.memo for Component Memoization

React.memo is a higher-order component that memoizes the result of a component. It only re-renders when its props change:

ReactUserCard.tsx
import React from 'react';interface User {  id: number;  name: string;  email: string;}interface UserCardProps {  user: User;  onEdit: (id: number) => void;}const UserCard = React.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>  );});UserCard.displayName = 'UserCard';export default UserCard;

useMemo for Expensive Calculations

useMemo helps prevent expensive calculations on every render by memoizing the result:

ReactProductList.tsx
import React, { useMemo, useState } from 'react';interface Product {  id: number;  name: string;  price: number;  category: string;}interface ProductListProps {  products: Product[];}export const ProductList: React.FC<ProductListProps> = ({ products }) => {  const [searchTerm, setSearchTerm] = useState('');  const [sortBy, setSortBy] = useState<'name' | 'price'>('name');  // Expensive filtering and sorting operation  const filteredAndSortedProducts = useMemo(() => {    console.log('Filtering and sorting products...');        let filtered = products.filter(product =>      product.name.toLowerCase().includes(searchTerm.toLowerCase())    );    return filtered.sort((a, b) => {      if (sortBy === 'name') {        return a.name.localeCompare(b.name);      }      return a.price - b.price;    });  }, [products, searchTerm, sortBy]);  return (    <div>      <input        type="text"        placeholder="Search products..."        value={searchTerm}        onChange={(e) => setSearchTerm(e.target.value)}      />            <select         value={sortBy}         onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}      >        <option value="name">Sort by Name</option>        <option value="price">Sort by Price</option>      </select>      <div className="products">        {filteredAndSortedProducts.map(product => (          <div key={product.id} className="product">            <h3>{product.name}</h3>            <p>${product.price}</p>          </div>        ))}      </div>    </div>  );};

useCallback for Function Memoization

useCallback prevents unnecessary re-creation of functions, which is especially important when passing callbacks to memoized child components:

ReactTodoApp.tsx
import React, { useState, useCallback } from 'react';import TodoItem from './TodoItem';interface Todo {  id: number;  text: string;  completed: boolean;}export const TodoApp: React.FC = () => {  const [todos, setTodos] = useState<Todo[]>([]);  // Without useCallback, this function is recreated on every render  // causing TodoItem to re-render even when memoized  const toggleTodo = useCallback((id: number) => {    setTodos(prevTodos =>      prevTodos.map(todo =>        todo.id === id ? { ...todo, completed: !todo.completed } : todo      )    );  }, []);  const deleteTodo = useCallback((id: number) => {    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));  }, []);  return (    <div>      {todos.map(todo => (        <TodoItem          key={todo.id}          todo={todo}          onToggle={toggleTodo}          onDelete={deleteTodo}        />      ))}    </div>  );};

Code Splitting with React.lazy

Code splitting allows you to split your code into smaller chunks that can be loaded on demand, reducing the initial bundle size:

ReactApp.tsx
import React, { Suspense, lazy } from 'react';import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';import LoadingSpinner from './components/LoadingSpinner';// Lazy load componentsconst Home = lazy(() => import('./pages/Home'));const Dashboard = lazy(() => import('./pages/Dashboard'));const Analytics = lazy(() => import('./pages/Analytics'));// You can also add a delay for better UXconst Profile = lazy(() =>   import('./pages/Profile').then(module => ({    default: module.default  })));export const App: React.FC = () => {  return (    <Router>      <div className="app">        <Suspense fallback={<LoadingSpinner />}>          <Routes>            <Route path="/" element={<Home />} />            <Route path="/dashboard" element={<Dashboard />} />            <Route path="/analytics" element={<Analytics />} />            <Route path="/profile" element={<Profile />} />          </Routes>        </Suspense>      </div>    </Router>  );};

Virtualization for Large Lists

When rendering large lists, virtualization ensures only visible items are rendered in the DOM:

ReactVirtualizedList.tsx
import React from 'react';import { FixedSizeList as List } from 'react-window';interface Item {  id: number;  name: string;  description: string;}interface ItemRendererProps {  index: number;  style: React.CSSProperties;  data: Item[];}const ItemRenderer: React.FC<ItemRendererProps> = ({ index, style, data }) => (  <div style={style} className="list-item">    <h4>{data[index].name}</h4>    <p>{data[index].description}</p>  </div>);interface VirtualizedListProps {  items: Item[];}export const VirtualizedList: React.FC<VirtualizedListProps> = ({ items }) => {  return (    <List      height={600}        // Container height      itemCount={items.length}      itemSize={100}      // Height of each item      itemData={items}      width="100%"    >      {ItemRenderer}    </List>  );};

Performance Profiling with React DevTools

The React DevTools Profiler helps identify performance bottlenecks in your application:

  • Install React DevTools browser extension
  • Open the Profiler tab
  • Click "Start profiling" and interact with your app
  • Stop profiling and analyze the flame graph
  • Look for components with long render times
  • Identify unnecessary re-renders with the "Why did this render?" feature
ReactProfilingExample.tsx
import React, { Profiler, ProfilerOnRenderCallback } from 'react';const onRenderCallback: ProfilerOnRenderCallback = (  id,  phase,  actualDuration,  baseDuration,  startTime,  commitTime) => {  console.log('Profiler:', {    id,    phase,    actualDuration,    baseDuration,    startTime,    commitTime  });};export const ProfilingExample: React.FC = () => {  return (    <Profiler id="App" onRender={onRenderCallback}>      <div>        {/* Your app components */}      </div>    </Profiler>  );};

Bundle Analysis and Optimization

Analyze your bundle size to identify optimization opportunities:

Terminal
# Install bundle analyzernpm install --save-dev webpack-bundle-analyzer# For Create React Appnpm install --save-dev source-map-explorer# Analyze bundle (CRA)npm run buildnpx source-map-explorer 'build/static/js/*.js'# For custom webpack confignpx webpack-bundle-analyzer build/static/js/*.js

Best Practices Summary

Memoization

Use React.memo, useMemo, and useCallback to prevent unnecessary re-renders and computations.

Code Splitting

Implement lazy loading with React.lazy and Suspense to reduce initial bundle size.

Virtualization

Use react-window or react-virtualized for large lists to render only visible items.

Bundle Optimization

Analyze and optimize your bundle size, remove unused dependencies, and use tree shaking.

Performance Monitoring

Profile your app before making optimizations
Focus on the biggest performance bottlenecks first
Measure the impact of each optimization
Don't over-optimize at the expense of code readability