# Performance Optimization

Course: [React](https://www.devtutorials.ai/learn/react) · Section: Application Quality
Source: https://www.devtutorials.ai/learn/react/quality/performance-optimization

> Why performance matters, how React renders, avoiding extra re-renders, memoization with React.memo, useMemo and useCallback, lazy loading, code splitting and best practices.

## Why Performance Matters

**Performance** means how fast your app feels. A fast app opens quickly, and reacts at once when the user clicks or types.

Speed matters because people leave slow apps. It matters most on cheap phones and slow networks, which many of your users may have.

There are two kinds of slowness:

- **Slow loading.** The app downloads too much code before it can show anything.
- **Slow updates.** The app does too much work each time something changes.

This lesson shows simple ways to fix both.

### Real-World Example

A bookshop's search page shows 500 books. Each key press in the search box rendered all 500 cards again, so typing felt slow.

The team wraps `BookCard` in `memo`, sorts the list with `useMemo`, and passes handlers made with `useCallback`. Now a key press renders only the cards that changed.

Most React apps are fast enough without any tricks. First make your code correct and clear. Optimize only when something feels slow, and measure before and after.

## React Rendering and Performance

To **render** means React calls your component function to get its TSX. Then React compares the new result with the old one, and changes only the parts of the real page that are different.

A component renders again when:

- its own **state** changes,
- its **parent** renders again, or
- a **context** it reads changes.

The second point surprises many beginners. When a parent renders, **all** its children render too, even if their props did not change.

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-r-rerender" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-r-rerender">When the parent's state changes, React re-renders the parent and every child inside it</title>
  <rect x="185" y="12" width="150" height="44" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="260.0" y="31.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>App</text>
  <text x="260.0" y="48.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>state: count</text>
  <rect x="10" y="110" width="150" height="44" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="85.0" y="137.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Header</text>
  <rect x="185" y="110" width="150" height="44" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="260.0" y="137.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Counter</text>
  <rect x="360" y="110" width="150" height="44" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="435.0" y="137.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>BookList</text>
  <path d="M230 56 L90 106" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M90 106 L96.6 99.9 L99.0 106.5 Z" style={{ fill: "var(--faint)" }} />
  <path d="M260 56 L260 106" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M260 106 L256.5 97.7 L263.5 97.7 Z" style={{ fill: "var(--faint)" }} />
  <path d="M290 56 L430 106" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M430 106 L421.0 106.5 L423.4 99.9 Z" style={{ fill: "var(--faint)" }} />
  <rect x="185" y="12" width="150" height="44" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3s" begin="0s" repeatCount="indefinite" />
  </rect>
  <rect x="10" y="110" width="150" height="44" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3s" begin="0.3s" repeatCount="indefinite" />
  </rect>
  <rect x="185" y="110" width="150" height="44" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3s" begin="0.3s" repeatCount="indefinite" />
  </rect>
  <rect x="360" y="110" width="150" height="44" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3s" begin="0.3s" repeatCount="indefinite" />
  </rect>
</svg>
<figcaption>Only Counter shows the count, but all three children render again.</figcaption>
</figure>

Each component below prints a line every time it renders. Click the button and watch the console. `Header` and `BookList` render again, even though nothing in them changed.

      <li>The Alchemist</li>
      <li>Wings of Fire</li>
    </ul>
  );
}

  const [count, setCount] = useState(0);
  console.log("App rendered");

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Cart: {count}</button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

## Avoiding Unnecessary Re-renders

A re-render is **unnecessary** when it gives the same result as before. A few renders do no harm. They become a problem when a component is big or slow, such as a list with thousands of rows.

Simple ways to avoid them, before any special tools:

- **Keep state low.** Put state in the smallest component that needs it. Then only that component re-renders.
- **Do not copy data into state** if you can work it out from props or other state.
- **Use stable keys** in lists (like `book.id`), so React can reuse rows.

<figure className="react-fig">
<svg viewBox="0 0 520 150" role="img" aria-labelledby="fig-r-tree" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-r-tree">Keep fast-changing state low: move it into the small component that uses it</title>
  <text x="130" y="20" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>State at the top</text>
  <text x="390" y="20" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>State moved down</text>
  <rect x="70" y="32" width="120" height="36" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="130.0" y="55.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>App (query)</text>
  <rect x="10" y="100" width="110" height="36" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="65.0" y="123.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>SearchBox</text>
  <rect x="140" y="100" width="110" height="36" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="195.0" y="123.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>BigList</text>
  <path d="M110 68 L65 96" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M65 96 L70.2 88.6 L73.9 94.6 Z" style={{ fill: "var(--faint)" }} />
  <path d="M150 68 L195 96" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M195 96 L186.1 94.6 L189.8 88.6 Z" style={{ fill: "var(--faint)" }} />
  <rect x="10" y="100" width="110" height="36" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="2.5s" begin="0s" repeatCount="indefinite" />
  </rect>
  <rect x="140" y="100" width="110" height="36" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="2.5s" begin="0s" repeatCount="indefinite" />
  </rect>
  <rect x="330" y="32" width="120" height="36" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="390.0" y="55.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>App</text>
  <rect x="270" y="100" width="110" height="36" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="325.0" y="115.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>SearchBox</text>
  <text x="325.0" y="132.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>has query</text>
  <rect x="400" y="100" width="110" height="36" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="455.0" y="123.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>BigList</text>
  <path d="M370 68 L325 96" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M325 96 L330.2 88.6 L333.9 94.6 Z" style={{ fill: "var(--faint)" }} />
  <path d="M410 68 L455 96" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M455 96 L446.1 94.6 L449.8 88.6 Z" style={{ fill: "var(--faint)" }} />
  <rect x="270" y="100" width="110" height="36" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="2.5s" begin="0s" repeatCount="indefinite" />
  </rect>
</svg>
<figcaption>Typing now re-renders only SearchBox, not the big list.</figcaption>
</figure>

Below, the search text lives in `SearchBox`, not in `App`. Type in the box. Only `SearchBox` renders. `BookList` stays quiet.

      <li>The Alchemist</li>
      <li>Atomic Habits</li>
    </ul>
  );
}

  console.log("App rendered");
  return (
    <div>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

## Memoization Techniques

### What is Memoization

**Memoization** means remembering a result so you do not work it out again. If the inputs are the same as last time, you reuse the old answer.

React gives you three tools for this:

- `React.memo` remembers a **component's output**. It skips a render when the props are the same.
- `useMemo` remembers a **value**, such as a filtered list.
- `useCallback` remembers a **function**, so it stays the same between renders.

### React.memo

Wrap a component in `memo` to skip renders when its props have not changed:

```tsx
const BookList = memo(function BookList({ books }: Props) { ... });
```

Before each render, React compares the old props with the new props. If every prop is the same, React reuses the last result.

<figure className="react-fig">
<svg viewBox="0 0 520 150" role="img" aria-labelledby="fig-r-memo" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-r-memo">React.memo compares the old and new props. Same props: skip. Different props: render</title>
  <rect x="10" y="50" width="120" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="70.0" y="80.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>new render</text>
  <path d="M130 75 L170 75" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M170 75 L161.7 78.5 L161.7 71.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="175" y="45" width="150" height="60" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="250.0" y="72.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>React.memo</text>
  <text x="250.0" y="89.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>props the same?</text>
  <path d="M325 62 L370 28" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M370 28 L365.5 35.8 L361.3 30.2 Z" style={{ fill: "var(--faint)" }} />
  <text x="347.5" y="36" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>yes</text>
  <rect x="375" y="6" width="140" height="44" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="445.0" y="33.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>skip, reuse old</text>
  <path d="M325 88 L370 122" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M370 122 L361.3 119.8 L365.5 114.2 Z" style={{ fill: "var(--faint)" }} />
  <text x="347.5" y="116" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>no</text>
  <rect x="375" y="100" width="140" height="44" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="445.0" y="127.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>render again</text>
  <circle r="6" style={{ fill: "var(--ok)" }}>
    <animateMotion dur="2.4s" repeatCount="indefinite" path="M130 75 L170 75 M325 62 L370 28" />
  </circle>
</svg>
<figcaption>Memo saves work only when the props really stay the same.</figcaption>
</figure>

      <button onClick={() => setCount(count + 1)}>Cart: {count}</button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Click the button a few times. `App` renders each time, but `Header` renders only once, because its prop `shop` is always `"Book Corner"`.

There is a trap. React compares props with `===`. A new object or a new function is created on **every** render, so it is never `===` the old one. Then `memo` does nothing.

- `useMemo(() => value, [deps])` keeps the same object until a dependency changes.
- `useCallback(fn, [deps])` keeps the same function until a dependency changes.

In the lab below, `onSelect` is wrapped in `useCallback`. Remove `useCallback` and see `BookList` render on every click.

      {books.map((b) => (
        <li key={b.id} onClick={() => onSelect(b.id)}>{b.title}</li>
      ))}
    </ul>
  );
});

  const [count, setCount] = useState(0);
  const [maxPrice, setMaxPrice] = useState(400);

  // same list until maxPrice changes
  const cheapBooks = useMemo(() => allBooks.filter((b) => b.price <= maxPrice), [maxPrice]);

  // same function on every render
  const handleSelect = useCallback((id: number) => {
    console.log("Selected book " + id);
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Cart: {count}</button>{" "}
      <button onClick={() => setMaxPrice(maxPrice === 400 ? 500 : 400)}>Max price: {maxPrice}</button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Do not wrap everything in `memo`, `useMemo` and `useCallback`. Each one has a small cost too. Use them for big or slow components, or when the console shows many wasted renders.

## Loading and Bundle Optimization

When you build a React app, the build tool joins all your code into one or more JavaScript files called **bundles**. The browser must download a bundle before it can show the page. A big bundle means a slow start.

### Lazy Loading Components

**Lazy loading** means loading a component only when it is needed, not at the start. React gives you `lazy` and `Suspense` for this:

```tsx
const Reviews = lazy(() => import("./Reviews"));
```

- `import("./Reviews")` (with brackets) loads the file later, when the code runs. The file must have a `default` export.
- `lazy(...)` turns that into a component you can use like any other.
- `<Suspense fallback={...}>` shows the fallback while the component is still loading.

      <h2>Atomic Habits</h2>
      <button onClick={() => setShow(true)}>Show reviews</button>
      {show && (
        <Suspense fallback={<p>Loading reviews...</p>}>
      )}
    </div>
  );
}
`,
    "Reviews.tsx": `console.log("Reviews file loaded");

  return (
    <ul>
      <li>"Small changes, big results." ★★★★★</li>
      <li>"Easy to read." ★★★★</li>
    </ul>
  );
}
`,
    "slowImport.ts": `export function slowImport<T>(load: () => Promise<T>): Promise<T> {
  // pretend the network is slow: wait 1 second, then load
  return new Promise<void>((r) => setTimeout(r, 1000)).then(load);
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

The playground loads files at once, so `slowImport.ts` waits one second to act like a slow network. In a real app, you write `lazy(() => import("./Reviews"))` and the network gives you the wait.

### Code Splitting

**Code splitting** means breaking the app's code into several smaller bundles. Each `import()` with brackets tells the build tool (like Vite): "put this file in its own bundle". So `lazy` is how you split code in React.

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-r-split" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-r-split">Code splitting: the first download is small, and other parts load only when needed</title>
  <text x="130" y="20" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>One big bundle</text>
  <text x="390" y="20" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Split bundles</text>
  <rect x="20" y="32" width="220" height="120" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="130.0" y="89.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>app.js</text>
  <text x="130.0" y="106.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>everything: 900 KB</text>
  <rect x="280" y="32" width="110" height="56" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="335.0" y="57.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>main.js</text>
  <text x="335.0" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>200 KB</text>
  <g opacity="0.25">
  <rect x="400" y="32" width="110" height="56" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="455.0" y="57.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>reviews.js</text>
  <text x="455.0" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>loads later</text>
  <rect x="280" y="96" width="230" height="56" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="395.0" y="121.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>admin.js</text>
  <text x="395.0" y="138.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>loads later</text>
    <animate attributeName="opacity" values="0.25;0.25;1;1;0.25" keyTimes="0;0.4;0.5;0.9;1" dur="5s" repeatCount="indefinite" />
  </g>
</svg>
<figcaption>The user sees the page sooner, because the first file is smaller.</figcaption>
</figure>

The most common place to split is by **page**. Each route gets its own bundle, and a user downloads a page only when they visit it.

```tsx
const AdminPage = lazy(() => import("./pages/AdminPage"));

<Suspense fallback={<p>Loading...</p>}>
  <Routes>
    <Route path="/admin" element={<AdminPage />} />
  </Routes>
</Suspense>
```

## Component Splitting Strategies

How you split components also affects speed. Some simple rules:

- **Split by job.** A component that does one thing is easier to memo and to reuse.
- **Separate fast parts from slow parts.** Put a quickly changing input in its own small component, away from a big list.
- **Pass children.** A wrapper that takes `children` does not re-render them when its own state changes, because the children were made by the parent.
- **Lazy-load rare parts.** Dialogs, charts and admin pages that few users open can load later.

The lab below uses the `children` pattern. `ColorBox` has its own state, but `BigList`, passed as children, does not re-render when the colour changes.

      <button onClick={() => setOn(!on)}>Change colour</button>
      {children}
    </div>
  );
}

function BigList() {
  console.log("BigList rendered");
  return (
    <ul>
      <li>The Alchemist</li>
      <li>Wings of Fire</li>
      <li>Atomic Habits</li>
    </ul>
  );
}

  return (
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

## Performance Optimization Best Practices

- **Measure first.** Use the **Profiler** tab in React Developer Tools to see which components render and how long they take.
- **Build for production** to test speed. `npm run dev` adds extra checks and is slower than `npm run build`.
- **Keep state close** to where it is used.
- **Give list items stable keys**, like an id, not the array index.
- **Memoize only slow parts** with `memo`, `useMemo` and `useCallback`.
- **Lazy-load pages and rare parts** so the first bundle is small.
- **Show long lists in pages**, or only the rows on screen, instead of thousands at once.
- **Optimize images**: use the right size and a modern format.

Easy Way to Remember: measure, keep state low, memo the slow parts, lazy-load the rest.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| Wrapping every component in `memo` | More code, no speed gain | Memo only big or slow components |
| Passing a new object or arrow function to a memo component | It still renders every time | Use `useMemo` or `useCallback` |
| Missing items in a `useMemo` dependency list | Old values show on screen | List every value used inside |
| Keeping input state at the top of the app | Typing feels slow in big pages | Move the state into the input's component |
| Using `lazy` without `Suspense` | An error when the component loads | Wrap it in `<Suspense fallback={...}>` |
| Lazy-loading a file with only named exports | An error: no default export | Add `export default` to the component |
| Testing speed with `npm run dev` | The app looks slower than it is | Test a production build |

</div>

## Check Yourself

## Exercises

## What to Remember

- A component renders when its state, its parent or its context changes. Keep state low to keep renders small.
- `memo` skips renders when props are the same. `useMemo` and `useCallback` keep objects and functions the same.
- `lazy` + `Suspense` load parts of the app later. This is code splitting, and it makes the first load faster. Measure before you optimize.

Next: Capstone - Task Management
