Back to Tutorial Overview

React Performance Optimization

Build lightning-fast React applications with proven optimization techniques

Section 1: Component Memoization with React.memo

The Problem: Unnecessary Re-renders

Without memoization, child components re-render whenever parent re-renders, even if props haven't changed:

// ❌ Bad: Child re-renders on every parent update
function ExpensiveChild({ data }) {
  console.log('ExpensiveChild rendered');
  // Expensive calculations here
  return <div>{data.value}</div>;
}

function Parent() {
  const [count, setCount] = useState(0);
  const data = { value: 'constant' };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      <ExpensiveChild data={data} />
      {/* Child re-renders even though data hasn't changed! */}
    </div>
  );
}

The Solution: React.memo

// ✅ Good: Child only re-renders when props change
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
  console.log('ExpensiveChild rendered');
  return <div>{data.value}</div>;
});

// With custom comparison function
const ExpensiveChild = React.memo(
  function ExpensiveChild({ user }) {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return prevProps.user.id === nextProps.user.id;
  }
);

Section 2: Memoizing Values with useMemo

When to Use useMemo

Use useMemo for expensive calculations that don't need to run on every render:

import { useMemo } from 'react';

function ProductList({ products, filterText }) {
  // ❌ Bad: Expensive filter runs on every render
  const filteredProducts = products.filter(p => 
    p.name.toLowerCase().includes(filterText.toLowerCase())
  );
  
  // ✅ Good: Filter only runs when dependencies change
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');
    return products.filter(p => 
      p.name.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [products, filterText]);
  
  return (
    <div>
      {filteredProducts.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

Real-World Examples

// Example 1: Expensive computation
function DataAnalysis({ data }) {
  const analysis = useMemo(() => {
    const sum = data.reduce((a, b) => a + b, 0);
    const avg = sum / data.length;
    const max = Math.max(...data);
    const min = Math.min(...data);
    return { sum, avg, max, min };
  }, [data]);
  
  return <AnalysisDisplay {...analysis} />;
}

// Example 2: Stabilizing object references
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  // ✅ Good: Options object stays stable
  const fetchOptions = useMemo(() => ({
    headers: { 'Authorization': `Bearer ${token}` },
    method: 'GET'
  }), [token]);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`, fetchOptions)
      .then(res => res.json())
      .then(setUser);
  }, [userId, fetchOptions]);
  
  return <div>{user?.name}</div>;
}

Section 3: Memoizing Functions with useCallback

Why useCallback Matters

import { useCallback } from 'react';

const SearchableList = React.memo(function SearchableList({ onSearch }) {
  return <input onChange={(e) => onSearch(e.target.value)} />;
});

function Parent() {
  const [count, setCount] = useState(0);
  
  // ❌ Bad: New function on every render breaks memoization
  const handleSearch = (query) => {
    console.log('Searching:', query);
  };
  
  // ✅ Good: Stable function reference
  const handleSearch = useCallback((query) => {
    console.log('Searching:', query);
  }, []); // Empty deps = function never changes
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      <SearchableList onSearch={handleSearch} />
      {/* SearchableList doesn't re-render when count changes */}
    </div>
  );
}

Practical Examples

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');
  
  // Memoize handlers passed to child components
  const handleAddTodo = useCallback((text) => {
    setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
  }, []);
  
  const handleToggleTodo = useCallback((id) => {
    setTodos(prev => prev.map(todo => 
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  }, []);
  
  const handleDeleteTodo = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);
  
  return (
    <div>
      <AddTodoForm onAdd={handleAddTodo} />
      <TodoList 
        todos={todos} 
        onToggle={handleToggleTodo}
        onDelete={handleDeleteTodo}
      />
    </div>
  );
}

Section 4: Code Splitting with React.lazy

Lazy Loading Components

import { lazy, Suspense } from 'react';

// ❌ Bad: Load everything upfront
import HeavyChart from './HeavyChart';
import HeavyDataTable from './HeavyDataTable';
import HeavyEditor from './HeavyEditor';

// ✅ Good: Load on demand
const HeavyChart = lazy(() => import('./HeavyChart'));
const HeavyDataTable = lazy(() => import('./HeavyDataTable'));
const HeavyEditor = lazy(() => import('./HeavyEditor'));

function Dashboard() {
  const [activeTab, setActiveTab] = useState('chart');
  
  return (
    <div>
      <Tabs value={activeTab} onChange={setActiveTab}>
        <Tab value="chart">Chart</Tab>
        <Tab value="table">Table</Tab>
        <Tab value="editor">Editor</Tab>
      </Tabs>
      
      <Suspense fallback={<LoadingSpinner />}>
        {activeTab === 'chart' && <HeavyChart />}
        {activeTab === 'table' && <HeavyDataTable />}
        {activeTab === 'editor' && <HeavyEditor />}
      </Suspense>
    </div>
  );
}

Route-Based Code Splitting

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Lazy load routes
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={
        <div className="loading">
          <Spinner />
          <p>Loading page...</p>
        </div>
      }>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile" element={<Profile />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Section 5: Optimizing Long Lists

Using react-window for Virtualization

Install react-window: npm install react-window

import { FixedSizeList } from 'react-window';

// ❌ Bad: Rendering 10,000 items
function LargeList({ items }) {
  return (
    <div>
      {items.map(item => (
        <div key={item.id} style={{ height: 50 }}>
          {item.name}
        </div>
      ))}
    </div>
  );
}

// ✅ Good: Only render visible items
function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

// Variable size rows
import { VariableSizeList } from 'react-window';

function DynamicList({ items }) {
  const getItemSize = (index) => items[index].height || 50;
  
  return (
    <VariableSizeList
      height={600}
      itemCount={items.length}
      itemSize={getItemSize}
      width="100%"
    >
      {Row}
    </VariableSizeList>
  );
}

Section 6: Using React DevTools Profiler

Identifying Performance Issues

1. Open React DevTools

Install the React DevTools extension and open the Profiler tab

2. Record a Session

Click record, interact with your app, then stop recording

3. Analyze Results

Look for components with long render times or frequent re-renders

Programmatic Profiling

import { Profiler } from 'react';

function onRenderCallback(
  id,              // the "id" prop of the Profiler tree
  phase,           // "mount" or "update"
  actualDuration,  // time spent rendering
  baseDuration,    // estimated time without memoization
  startTime,       // when React began rendering
  commitTime,      // when React committed the update
  interactions     // Set of interactions
) {
  console.log(`${id} took ${actualDuration}ms to render`);
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Dashboard />
    </Profiler>
  );
}

Keep Optimizing!

Performance optimization is an ongoing process. Always measure before optimizing!

Back to Tutorial Overview