Learn React in 10 DaysDay 9: Performance Optimization
books.chapter 9Learn React in 10 Days

Day 9: Performance Optimization

What You'll Learn Today

  • How re-rendering works
  • Memoizing components with React.memo
  • Memoizing values with useMemo
  • Memoizing functions with useCallback
  • Code splitting and Lazy Loading

How Re-rendering Works

In React, components re-render when their State or Props change.

flowchart TB
    subgraph Rerender["Re-render Triggers"]
        A["State update"]
        B["Props change"]
        C["Parent re-render"]
    end

    subgraph Result["Result"]
        D["Component function re-executes"]
        E["Child components also re-render"]
    end

    A --> D
    B --> D
    C --> D
    D --> E

    style A fill:#3b82f6,color:#fff
    style B fill:#8b5cf6,color:#fff
    style C fill:#f59e0b,color:#fff

Re-rendering Example

function Parent() {
  const [count, setCount] = useState(0);
  console.log('Parent rendered');

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Child />  {/* Re-renders every time */}
    </div>
  );
}

function Child() {
  console.log('Child rendered');  // Runs whenever parent updates
  return <p>I am a child</p>;
}
TypeScript version
function Parent(): React.JSX.Element {
  const [count, setCount] = useState<number>(0);
  console.log('Parent rendered');

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Child />  {/* Re-renders every time */}
    </div>
  );
}

function Child(): React.JSX.Element {
  console.log('Child rendered');  // Runs whenever parent updates
  return <p>I am a child</p>;
}

React.memo

React.memo is a higher-order component that skips re-rendering when Props haven't changed.

Basic Usage

import { memo } from 'react';

// Wrap with memo()
const Child = memo(function Child({ name }) {
  console.log('Child rendered');
  return <p>Hello, {name}</p>;
});

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Child name="Alice" />  {/* Won't re-render unless name changes */}
    </div>
  );
}
TypeScript version
import { memo } from 'react';

interface ChildProps {
  name: string;
}

// Wrap with memo()
const Child = memo(function Child({ name }: ChildProps): React.JSX.Element {
  console.log('Child rendered');
  return <p>Hello, {name}</p>;
});

function Parent(): React.JSX.Element {
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Child name="Alice" />  {/* Won't re-render unless name changes */}
    </div>
  );
}

Custom Comparison Function

const UserCard = memo(
  function UserCard({ user }) {
    return (
      <div>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
      </div>
    );
  },
  // Custom comparison function
  (prevProps, nextProps) => {
    return prevProps.user.id === nextProps.user.id;
  }
);
TypeScript version
interface User {
  id: number;
  name: string;
  email: string;
}

interface UserCardProps {
  user: User;
}

const UserCard = memo(
  function UserCard({ user }: UserCardProps): React.JSX.Element {
    return (
      <div>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
      </div>
    );
  },
  // Custom comparison function
  (prevProps: UserCardProps, nextProps: UserCardProps) => {
    return prevProps.user.id === nextProps.user.id;
  }
);

React.memo Gotchas

// ❌ Objects and arrays create new references each time
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <Child
      style={{ color: 'red' }}  // New object every render
      items={[1, 2, 3]}         // New array every render
    />
  );
}

// βœ… Stabilize references with useMemo
function Parent() {
  const [count, setCount] = useState(0);

  const style = useMemo(() => ({ color: 'red' }), []);
  const items = useMemo(() => [1, 2, 3], []);

  return <Child style={style} items={items} />;
}

useMemo

useMemo is a hook that memoizes expensive calculations.

Basic Usage

import { useMemo, useState } from 'react';

function ExpensiveComponent({ items, filter }) {
  // ❌ Calculated every render
  const filteredItems = items.filter(item => item.includes(filter));

  // βœ… Only recalculated when filter or items change
  const filteredItems = useMemo(() => {
    console.log('Filtering...');
    return items.filter(item => item.includes(filter));
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => <li key={item}>{item}</li>)}
    </ul>
  );
}
TypeScript version
import { useMemo, useState } from 'react';

interface ExpensiveComponentProps {
  items: string[];
  filter: string;
}

function ExpensiveComponent({ items, filter }: ExpensiveComponentProps): React.JSX.Element {
  // βœ… Only recalculated when filter or items change
  const filteredItems = useMemo<string[]>(() => {
    console.log('Filtering...');
    return items.filter(item => item.includes(filter));
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => <li key={item}>{item}</li>)}
    </ul>
  );
}

Appropriate Use Cases

function DataTable({ data, sortKey, sortOrder }) {
  // Sorting can be expensive
  const sortedData = useMemo(() => {
    console.log('Sorting data...');
    return [...data].sort((a, b) => {
      const comparison = a[sortKey] > b[sortKey] ? 1 : -1;
      return sortOrder === 'asc' ? comparison : -comparison;
    });
  }, [data, sortKey, sortOrder]);

  return (
    <table>
      <tbody>
        {sortedData.map(row => (
          <tr key={row.id}>
            <td>{row.name}</td>
            <td>{row.value}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
TypeScript version
interface DataRow {
  id: number;
  name: string;
  value: number;
  [key: string]: string | number;
}

interface DataTableProps {
  data: DataRow[];
  sortKey: string;
  sortOrder: 'asc' | 'desc';
}

function DataTable({ data, sortKey, sortOrder }: DataTableProps): React.JSX.Element {
  // Sorting can be expensive
  const sortedData = useMemo<DataRow[]>(() => {
    console.log('Sorting data...');
    return [...data].sort((a, b) => {
      const comparison = a[sortKey] > b[sortKey] ? 1 : -1;
      return sortOrder === 'asc' ? comparison : -comparison;
    });
  }, [data, sortKey, sortOrder]);

  return (
    <table>
      <tbody>
        {sortedData.map(row => (
          <tr key={row.id}>
            <td>{row.name}</td>
            <td>{row.value}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

When useMemo Is Unnecessary

// ❌ useMemo not needed for simple calculations
const double = useMemo(() => count * 2, [count]);

// βœ… Calculate directly
const double = count * 2;

useCallback

useCallback is a hook that memoizes functions. It's mainly used when passing functions to memoized components.

Basic Usage

import { useCallback, memo, useState } from 'react';

const Button = memo(function Button({ onClick, children }) {
  console.log(`Button "${children}" rendered`);
  return <button onClick={onClick}>{children}</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ❌ New function created every render
  const handleClick = () => setCount(count + 1);

  // βœ… Returns same function unless dependencies change
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <Button onClick={handleIncrement}>Increment</Button>
    </div>
  );
}
TypeScript version
import { useCallback, memo, useState } from 'react';

interface ButtonProps {
  onClick: () => void;
  children: React.ReactNode;
}

const Button = memo(function Button({ onClick, children }: ButtonProps): React.JSX.Element {
  console.log(`Button "${children}" rendered`);
  return <button onClick={onClick}>{children}</button>;
});

function Parent(): React.JSX.Element {
  const [count, setCount] = useState<number>(0);
  const [text, setText] = useState<string>('');

  // βœ… Returns same function unless dependencies change
  const handleIncrement = useCallback((): void => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <Button onClick={handleIncrement}>Increment</Button>
    </div>
  );
}

Optimizing Event Handlers

function TodoList({ todos, onToggle, onDelete }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
}

const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});

function App() {
  const [todos, setTodos] = useState([]);

  // Memoize with useCallback
  const handleToggle = useCallback((id) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return <TodoList todos={todos} onToggle={handleToggle} onDelete={handleDelete} />;
}
TypeScript version
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoListProps {
  todos: Todo[];
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

function TodoList({ todos, onToggle, onDelete }: TodoListProps): React.JSX.Element {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
}

interface TodoItemProps {
  todo: Todo;
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }: TodoItemProps): React.JSX.Element {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});

function App(): React.JSX.Element {
  const [todos, setTodos] = useState<Todo[]>([]);

  // Memoize with useCallback
  const handleToggle = useCallback((id: number): void => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  const handleDelete = useCallback((id: number): void => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return <TodoList todos={todos} onToggle={handleToggle} onDelete={handleDelete} />;
}

Code Splitting and Lazy Loading

Load large components only when needed.

flowchart LR
    subgraph LazyLoading["Lazy Loading"]
        A["Initial load<br/>Main bundle"]
        B["Load on demand<br/>Additional chunks"]
    end

    A -->|"User action"| B

    style A fill:#3b82f6,color:#fff
    style B fill:#22c55e,color:#fff

React.lazy

import { lazy, Suspense, useState } from 'react';

// Dynamic import
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  const [showHeavy, setShowHeavy] = useState(false);

  return (
    <div>
      <button onClick={() => setShowHeavy(true)}>
        Show Heavy Component
      </button>

      {showHeavy && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}
TypeScript version
import { lazy, Suspense, useState } from 'react';

// Dynamic import
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App(): React.JSX.Element {
  const [showHeavy, setShowHeavy] = useState<boolean>(false);

  return (
    <div>
      <button onClick={() => setShowHeavy(true)}>
        Show Heavy Component
      </button>

      {showHeavy && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

Route-Based Splitting

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

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

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading page...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}
TypeScript version
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

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

function App(): React.JSX.Element {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading page...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Analysis with React DevTools

Using the Profiler

Analyze rendering performance with the Profiler tab in React DevTools.

// Debug markers
import { Profiler } from 'react';

function onRenderCallback(
  id,             // Profiler id
  phase,          // "mount" or "update"
  actualDuration, // Time spent rendering
  baseDuration,   // Estimated time without memoization
  startTime,      // When rendering started
  commitTime      // When commit happened
) {
  console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`);
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <MainContent />
    </Profiler>
  );
}
TypeScript version
// Debug markers
import { Profiler, ProfilerOnRenderCallback } from 'react';

const onRenderCallback: ProfilerOnRenderCallback = (
  id,             // Profiler id
  phase,          // "mount" or "update"
  actualDuration, // Time spent rendering
  baseDuration,   // Estimated time without memoization
  startTime,      // When rendering started
  commitTime      // When commit happened
) => {
  console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`);
};

function App(): React.JSX.Element {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <MainContent />
    </Profiler>
  );
}

Optimization Guidelines

When to Optimize

flowchart TB
    A["Performance issue?"] --> B{"Measured?"}
    B -->|No| C["Measure first"]
    B -->|Yes| D{"Bottleneck identified?"}
    D -->|No| C
    D -->|Yes| E["Apply optimization"]
    C --> B

    style C fill:#f59e0b,color:#fff
    style E fill:#22c55e,color:#fff

Optimization Checklist

Check Solution
Unnecessary re-renders React.memo
Expensive calculations useMemo
Function recreation useCallback
Large bundles lazy + Suspense
Long lists Virtualization (react-window)

Avoid Over-Optimization

// ❌ Over-optimization
function Component() {
  // useMemo not needed for simple values
  const greeting = useMemo(() => 'Hello', []);

  // useCallback not needed for simple functions
  const log = useCallback(() => console.log('clicked'), []);

  return <div>{greeting}</div>;
}

// βœ… Optimize only when needed
function Component() {
  const greeting = 'Hello';
  const log = () => console.log('clicked');

  return <div onClick={log}>{greeting}</div>;
}

Practical Optimization Example

List Optimization

import { memo, useCallback, useMemo, useState } from 'react';

const ListItem = memo(function ListItem({ item, onSelect }) {
  console.log(`ListItem ${item.id} rendered`);
  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name}
    </li>
  );
});

function OptimizedList({ items }) {
  const [selected, setSelected] = useState(null);
  const [filter, setFilter] = useState('');

  // Memoize filtered results
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  // Memoize callback
  const handleSelect = useCallback((id) => {
    setSelected(id);
  }, []);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter"
      />
      <ul>
        {filteredItems.map(item => (
          <ListItem
            key={item.id}
            item={item}
            onSelect={handleSelect}
          />
        ))}
      </ul>
      {selected && <p>Selected: {selected}</p>}
    </div>
  );
}
TypeScript version
import { memo, useCallback, useMemo, useState } from 'react';

interface Item {
  id: number;
  name: string;
}

interface ListItemProps {
  item: Item;
  onSelect: (id: number) => void;
}

const ListItem = memo(function ListItem({ item, onSelect }: ListItemProps): React.JSX.Element {
  console.log(`ListItem ${item.id} rendered`);
  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name}
    </li>
  );
});

interface OptimizedListProps {
  items: Item[];
}

function OptimizedList({ items }: OptimizedListProps): React.JSX.Element {
  const [selected, setSelected] = useState<number | null>(null);
  const [filter, setFilter] = useState<string>('');

  // Memoize filtered results
  const filteredItems = useMemo<Item[]>(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  // Memoize callback
  const handleSelect = useCallback((id: number): void => {
    setSelected(id);
  }, []);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter"
      />
      <ul>
        {filteredItems.map(item => (
          <ListItem
            key={item.id}
            item={item}
            onSelect={handleSelect}
          />
        ))}
      </ul>
      {selected && <p>Selected: {selected}</p>}
    </div>
  );
}

Summary

Concept Description
Re-rendering Component function re-executes on State/Props change
React.memo Skip re-render if Props unchanged
useMemo Memoize calculated values
useCallback Memoize functions
lazy/Suspense Load components on demand

Key Takeaways

  1. Measure first, then identify bottlenecks
  2. React.memo prevents unnecessary child re-renders
  3. useMemo only for expensive calculations
  4. useCallback for functions passed to memoized components
  5. Over-optimization just complicates code

Exercises

Exercise 1: Basics

Create a component displaying a large list (1000 items) and optimize it with React.memo. Clicking an item should select it.

Exercise 2: Application

Create a product list with search functionality. Cache filtered results with useMemo to optimize performance.

Challenge

Create a dashboard with multiple sections that switch via tabs. Use lazy loading for each section to reduce initial load time.


References


Coming Up Next: On Day 10, we'll learn about "Custom Hooks and Styling." Understand logic reuse and styling approaches in React.