Lesson 37 of 45 advanced

Performance Optimization

Measure first, then cut the work React does and the bytes the browser downloads

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

You wrap a child in React.memo but it still re-renders on every parent render. What is the most likely cause?

Open interactive version (quiz + challenge)

Real-world analogy

A delivery van that feels slow could be slow for many reasons. The load may be heavy, there may be too many stops, or the route may be bad. A driver who guesses might repaint the van, which changes nothing. A good driver checks the trip log first, finds the longest stop, and fixes that. The cheapest fix is one trip per street, so the van carries only what the next street needs. The trip log is the profiler, and the one trip per street is code splitting.

What is it?

React performance work has two sides. The first is render work: how many components run, and how often, after a change. The second is delivery: how many bytes the browser downloads, parses and runs before the page is usable. Tools like memo, useMemo and useCallback cut render work, and lazy loading and virtualization cut delivery and DOM size. All of them have a cost in complexity, so you apply them where a measurement points.

Real-world relevance

A dashboard feels slow when someone types in a filter box. The Profiler shows that every table row re-renders on each keystroke. The team memoises the row and passes a stable callback, and unchanged rows stop re-rendering. Later, the first load is slow. The bundle analysis shows the reports page and its chart library in the main file. Moving that route to lazy loading cuts the first download.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

What should you do first when a React page feels slow?

What does wrapping a route component in lazy and Suspense achieve?

Code example

import { lazy, memo, Suspense, useCallback, useState } from 'react';
type Row = { id: number; name: string };

const RowItem = memo(function RowItem(
  { row, onSelect }: { row: Row; onSelect: (id: number) => void }
) {
  return (
    <li>
      <button type="button" onClick={() => onSelect(row.id)}>{row.name}</button>
    </li>
  );
});
export function RowList({ rows }: { rows: Row[] }) {
  const [selected, setSelected] = useState<number | null>(null);
  const handleSelect = useCallback((id: number) => setSelected(id), []);

  return (
    <>
      <p>Selected: {selected ?? 'none'}</p>
      <ul>
        {rows.map(r => <RowItem key={r.id} row={r} onSelect={handleSelect} />)}
      </ul>
    </>
  );
}
// fragment: ReportsPage lives in its own file and has a default export
const ReportsPage = lazy(() => import('./ReportsPage'));

export function ReportsRoute() {
  return (
    <Suspense fallback={<p>Loading report...</p>}>
      <ReportsPage />
    </Suspense>
  );
}

Line-by-line walkthrough

  1. 1. This is a fragment of a file. RowItem is wrapped in memo, so React skips it if row and onSelect are the same objects as before.
  2. 2. The row button calls onSelect with its id. It is a real button, so the keyboard works.
  3. 3. RowList keeps the selected id in state. Changing it re-renders RowList.
  4. 4. handleSelect is wrapped in useCallback with no dependencies, so it is the same function on every render.
  5. 5. Each RowItem gets the same row object and the same callback, so memo skips the rows when selected changes.
  6. 6. ReportsPage is loaded with lazy, and ReportsRoute wraps it in Suspense, with a fallback shown while the code downloads.

Spot the bug

import { memo } from 'react';
type Item = { id: number; name: string };

const Row = memo(function Row({ item, style }: { item: Item; style: { color: string } }) {
  return <li style={style}>{item.name}</li>;
});

function List({ items }: { items: Item[] }) {
  return <ul>{items.map(i => <Row key={i.id} item={i} style={{ color: 'red' }} />)}</ul>;
}
Need a hint?
Is the style object the same object on each render of List?
Show answer
The inline style object is new on every render, so memo sees a changed prop and re-renders every Row anyway. Fix: define the style once outside the component, or use useMemo, so the reference stays the same.

Explain like I'm 5

If a toy truck is slow, first check what is making it slow. Do not just paint it. Then carry less in the truck, and fetch the heavy boxes only when someone asks for them.

Fun fact

Many slow pages are slow because of what they download, not because of how React renders. A single large library in the main bundle can cost more than all the re-renders together.

Hands-on challenge

Say it out loud in 30 seconds: "A React page is slow. What do you do?" Then check: did you mention measuring first, why components re-render, memo with stable props, code splitting, and virtualization?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook