Performance Optimization
โš›๏ธ React 18+ ๐ŸŸข Chapter 31 of 39 ๐Ÿ“‚ Phase 13: Performance ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter: memo() ยท useMemo() ยท useCallback() ยท Lazy Loading ยท Code Splitting ยท Suspense ยท Avoiding Unnecessary State ยท Virtualized Lists ยท Debouncing
With Chapter 30's mental model in place, this chapter covers React's actual optimization tools โ€” and, just as importantly, when you genuinely need them versus when they'd just add unnecessary complexity.
1Preventing Re-renders with memo()
๐Ÿ’ป Example 1: Skipping Unnecessary Child Re-renders
import { memo } from "react";

const Child = memo(function Child({ name }) {
  console.log("Child rendered");
  return <p>Hello, {name}</p>;
});
๐Ÿ” What memo() Does:

Wrapping a component in memo() tells React to skip re-rendering it if its props haven't actually changed since the last render โ€” directly solving the 'Child re-rendered even though it doesn't use count' scenario from Chapter 30.

2useMemo() and useCallback()
import { useMemo, useCallback } from "react";

function ProductList({ products, searchTerm }) {
  // useMemo - only recalculates when products or searchTerm actually change
  const filtered = useMemo(() => {
    return products.filter((p) => p.name.includes(searchTerm));
  }, [products, searchTerm]);

  // useCallback - returns the SAME function reference across renders, unless its dependencies change
  const handleSelect = useCallback((id) => {
    console.log("Selected:", id);
  }, []);

  return <ProductGrid items={filtered} onSelect={handleSelect} />;
}

useMemo caches an expensive calculated value; useCallback caches a function reference. Both matter most when passed down to a memo()-wrapped child โ€” without them, a parent re-render creates a brand-new array or function every time, defeating memo()'s comparison entirely.

3Lazy Loading and Code Splitting with Suspense
import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./Dashboard.jsx"));

function App() {
  return (
    <Suspense fallback={<p>Loading dashboard...</p>}>
      <Dashboard />
    </Suspense>
  );
}

lazy() splits a component into its own separate JavaScript file, only downloaded when it's actually needed โ€” for example, a Dashboard page only loaded after login, rather than bundled into the initial page load everyone downloads. <Suspense> shows a fallback UI while that chunk loads.

4Virtualized Lists and Debouncing

For lists with hundreds or thousands of items, rendering every single one at once โ€” even with memo() โ€” becomes slow. List virtualization (via libraries like react-window) renders only the items currently visible in the viewport, recycling DOM nodes as the user scrolls. And revisiting Chapter 22's search problem: the useDebounce custom hook built in Chapter 29 is itself a performance optimization โ€” reducing how often an expensive operation (an API call, a large filter) runs.

โš ๏ธ Reaching for memo/useMemo/useCallback Before Actually Measuring a Problem

These tools have real overhead of their own (extra comparisons on every render) and add code complexity. Wrapping every single component in memo() "just in case" often makes an app harder to reason about without any measurable benefit. The professional approach: build features normally first, then use React DevTools' Profiler to find actual, measured slowdowns before reaching for these optimizations.

๐Ÿ’ป Hands-on Interactive Practice Challenge

Wrap a child component in memo() and add a console.log inside it, then verify in the console that it no longer re-renders when an unrelated piece of parent state changes.

React Practice Challenge โ–ถ Run in Compiler
import { useState, memo } from "react";

const ExpensiveChild = memo(function ExpensiveChild() {
  console.log("ExpensiveChild rendered");
  return <p>I only re-render when my own props change.</p>;
});

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

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild />
    </>
  );
}
Run This Challenge in Online JS IDE โ†’
โ“ Frequently Asked Questions (FAQ)

Q Should I use memo() on every component by default?

No โ€” for most simple components, the cost of re-rendering is negligible, and adding memo() everywhere adds complexity without measurable benefit. Reserve it for components that are genuinely expensive to render or re-render very frequently with unchanged props.

Q What's the difference between lazy loading and useMemo?

They solve completely different problems: lazy loading defers downloading a component's code until it's needed (reducing initial bundle size), while useMemo caches the result of a calculation within an already-loaded component (reducing repeated computation).

OC
Written by Our Compiler Technical Editorial Team
Reviewed for accuracy & tested on React 18+ ยท Last updated August 2026