> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 10. Performance Optimization

> Master React performance optimization, profiling, and production-ready techniques.

# Performance Optimization

React is fast by default, but complex applications can slow down. This chapter covers techniques to identify bottlenecks and optimize your React apps.

**A critical mindset shift**: Optimization in React is not about making every component fast -- it is about making *slow components* faster. The Virtual DOM already prevents unnecessary real DOM updates. Most performance problems come from components re-rendering too often or doing too much work during a render. The tools in this chapter help you identify and fix those specific bottlenecks.

## When to Optimize

<Warning>
  **Premature optimization is the root of all evil.** Always measure first! Only optimize when you:

  1. Have actual performance issues users notice
  2. Have profiled and identified the bottleneck
  3. Can measure the improvement

  Most React apps don't need heavy optimization.
</Warning>

## Understanding Re-renders

Every time state or props change, React re-renders components. Understanding this is key to optimization.

```
┌─────────────────────────────────────────────────────────────┐
│                  Component Re-render Flow                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  State/Props Change                                         │
│         │                                                   │
│         ▼                                                   │
│  Component function runs again                              │
│         │                                                   │
│         ▼                                                   │
│  New JSX created                                            │
│         │                                                   │
│         ▼                                                   │
│  React diffs Virtual DOM                                    │
│         │                                                   │
│         ▼                                                   │
│  Only actual changes update real DOM                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### What Causes Re-renders?

1. **Component's state changes** (`useState`, `useReducer`)
2. **Parent component re-renders** -- this is the biggest surprise for beginners. When a parent re-renders, *all* of its children re-render by default, even if their props have not changed.
3. **Context value changes** -- every component that calls `useContext` re-renders when the provider's value changes.
4. **Custom hook state changes** -- if a hook uses `useState` internally, the component calling that hook re-renders when that state changes.

<Note>
  **Important clarification**: Re-rendering does not mean the real DOM is updated. React's diffing algorithm compares the new Virtual DOM with the old one and only applies actual DOM changes for the differences. Re-renders are usually fast. The problem arises when a re-render triggers expensive computations (filtering large arrays, complex calculations) or when thousands of components re-render unnecessarily.
</Note>

***

## React DevTools Profiler

The Profiler helps identify what's rendering and why.

### Using the Profiler

1. Install React DevTools browser extension
2. Open DevTools → Profiler tab
3. Click "Record" and interact with your app
4. Analyze the flame graph

### What to Look For

* **Frequent re-renders** of the same component
* **Slow components** (long render times)
* **Cascade re-renders** (parent update causing all children to update)

***

## React.memo - Prevent Unnecessary Re-renders

`React.memo` is a higher-order component that memoizes functional components.

### Basic Usage

```javascript theme={null}
// Without memo - re-renders whenever parent renders
function ExpensiveList({ items }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

// With memo - only re-renders when `items` changes
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
});
```

### Custom Comparison

```javascript theme={null}
const UserCard = React.memo(
  function UserCard({ user, onSelect }) {
    return (
      <div onClick={() => onSelect(user.id)}>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
      </div>
    );
  },
  // Custom comparison function
  (prevProps, nextProps) => {
    return prevProps.user.id === nextProps.user.id &&
           prevProps.user.name === nextProps.user.name;
  }
);
```

<Warning>
  `React.memo` does a **shallow comparison** by default. If you pass new object/array references every render, memoization won't work!

  ```javascript theme={null}
  // ❌ Bad - new array on every render
  <MemoizedComponent items={data.filter(x => x.active)} />

  // ✅ Good - memoize the filtered data
  const activeItems = useMemo(() => data.filter(x => x.active), [data]);
  <MemoizedComponent items={activeItems} />
  ```
</Warning>

***

## useMemo - Memoize Expensive Calculations

`useMemo` caches the result of expensive computations so they only run when their inputs change.

**Analogy**: `useMemo` is like a spreadsheet cell with a formula. The cell only recalculates when the cells it references change -- not every time you look at the spreadsheet. `useCallback` works the same way but for functions instead of values -- it is literally `useMemo(() => fn, deps)` under the hood.

```javascript theme={null}
function ProductList({ products, filter }) {
  // ❌ Without useMemo - runs on every render, even if
  // products and filter have not changed. For 10 items this
  // is fine, but for 10,000 items with complex sorting, it hurts.
  const filteredProducts = products
    .filter(p => p.category === filter)
    .sort((a, b) => a.price - b.price);

  // ✅ With useMemo - only recalculates when dependencies change.
  // React stores the previous result and returns it if
  // [products, filter] are the same as last render.
  const filteredProducts = useMemo(() => {
    console.log('Filtering and sorting...');
    return products
      .filter(p => p.category === filter)
      .sort((a, b) => a.price - b.price);
  }, [products, filter]);

  return (/* render list */);
}
```

### When to Use useMemo

| Use useMemo                           | Don't Use useMemo                   |
| ------------------------------------- | ----------------------------------- |
| Expensive calculations                | Simple math/string operations       |
| Large array operations                | Small arrays (fewer than 100 items) |
| Object creation for memoized children | Objects used once                   |
| Derived data from props/state         | Values passed to native elements    |

***

## useCallback - Memoize Functions

`useCallback` caches function references to prevent unnecessary re-renders of child components.

```javascript theme={null}
function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ❌ New function created every render
  const handleClick = () => {
    console.log('Clicked!');
  };

  // ✅ Same function reference unless dependencies change
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);

  // With dependencies
  const handleSubmit = useCallback(() => {
    submitForm(text);
  }, [text]);

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Counter count={count} />
      <MemoizedButton onClick={handleClick} />
    </div>
  );
}

const MemoizedButton = React.memo(function Button({ onClick }) {
  console.log('Button rendered');
  return <button onClick={onClick}>Click me</button>;
});
```

<Tip>
  **useCallback + React.memo = Optimization Power Combo**

  `useCallback` alone doesn't prevent re-renders. It only helps when:

  1. Passed to a `React.memo`-wrapped child component
  2. Used as a dependency in other hooks
</Tip>

***

## Code Splitting with React.lazy

Split your bundle so users only download what they need.

```javascript theme={null}
import { lazy, Suspense } from 'react';

// Lazy load components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
```

### Named Exports with Lazy

```javascript theme={null}
// For named exports, create an intermediate module
// utils/lazyImport.js
export function lazyImport(factory, name) {
  return lazy(() => 
    factory().then(module => ({ default: module[name] }))
  );
}

// Usage
const Dashboard = lazyImport(
  () => import('./pages/Dashboard'),
  'Dashboard'
);
```

***

## Virtualization for Long Lists

Only render visible items in long lists.

### Using react-window

```bash theme={null}
npm install react-window
```

```javascript theme={null}
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}
```

### Variable Size List

```javascript theme={null}
import { VariableSizeList } from 'react-window';

function VariableList({ items }) {
  const getItemSize = (index) => {
    return items[index].type === 'header' ? 80 : 50;
  };

  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].content}
    </div>
  );

  return (
    <VariableSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={getItemSize}
    >
      {Row}
    </VariableSizeList>
  );
}
```

***

## Debouncing and Throttling

Limit how often functions are called.

### Debouncing (Wait for pause)

```javascript theme={null}
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      searchAPI(debouncedQuery);
    }
  }, [debouncedQuery]);

  return (
    <input 
      value={query} 
      onChange={e => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}
```

### Throttling (Limit frequency)

```javascript theme={null}
function useThrottle(value, limit) {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastRan = useRef(Date.now());

  useEffect(() => {
    const handler = setTimeout(() => {
      if (Date.now() - lastRan.current >= limit) {
        setThrottledValue(value);
        lastRan.current = Date.now();
      }
    }, limit - (Date.now() - lastRan.current));

    return () => clearTimeout(handler);
  }, [value, limit]);

  return throttledValue;
}
```

***

## Image Optimization

### Lazy Loading Images

```javascript theme={null}
function LazyImage({ src, alt, ...props }) {
  return (
    <img 
      src={src} 
      alt={alt} 
      loading="lazy"  // Native lazy loading
      {...props}
    />
  );
}
```

### Progressive Loading with Blur

```javascript theme={null}
function ProgressiveImage({ lowQualitySrc, highQualitySrc, alt }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <div className="progressive-image">
      <img 
        src={lowQualitySrc}
        alt={alt}
        className={`low-quality ${loaded ? 'hidden' : ''}`}
        style={{ filter: 'blur(10px)' }}
      />
      <img 
        src={highQualitySrc}
        alt={alt}
        className={`high-quality ${loaded ? '' : 'hidden'}`}
        onLoad={() => setLoaded(true)}
      />
    </div>
  );
}
```

***

## Web Vitals

Key metrics for user experience:

| Metric                             | Description          | Target   |
| ---------------------------------- | -------------------- | -------- |
| **LCP** (Largest Contentful Paint) | Main content visible | \< 2.5s  |
| **FID** (First Input Delay)        | Time to interactive  | \< 100ms |
| **CLS** (Cumulative Layout Shift)  | Visual stability     | \< 0.1   |
| **TTFB** (Time to First Byte)      | Server response time | \< 600ms |

### Measuring Web Vitals

```javascript theme={null}
import { getCLS, getFID, getLCP } from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getLCP(console.log);

// Or send to analytics
function sendToAnalytics({ name, delta, id }) {
  analytics.send({
    metric: name,
    value: delta,
    id
  });
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
```

***

## Production Build Optimization

### Vite Production Build

```bash theme={null}
npm run build
npm run preview  # Preview production build locally
```

### Analyze Bundle Size

```bash theme={null}
# Install bundle analyzer
npm install -D rollup-plugin-visualizer

# vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    visualizer({ open: true })
  ]
});
```

### Environment-Specific Code

```javascript theme={null}
// Remove development-only code in production
if (import.meta.env.DEV) {
  console.log('Debug info:', data);
}
```

***

## Deployment

### Vercel (Recommended for React)

```bash theme={null}
npm install -g vercel
vercel
```

Or connect your GitHub repo at [vercel.com](https://vercel.com).

### Netlify

Create `netlify.toml`:

```toml theme={null}
[build]
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200
```

### Docker

```dockerfile theme={null}
# Build stage
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```

***

## Optimization Pitfalls

<Warning>
  **Pitfall 1 -- Memoizing everything "just in case"**: Wrapping every component in `React.memo` and every function in `useCallback` is not free. Each memoization adds overhead: React must store the previous props/value and run a comparison on every render. For simple components that render quickly, the comparison itself can cost more than just re-rendering. Only memoize when you have evidence of a performance problem.

  **Pitfall 2 -- Stale closures in useCallback**: When you use `useCallback` with an empty dependency array, the function captures the initial values of any variables from the component scope. If those values change, the memoized function still sees the old ones.

  ```javascript theme={null}
  function SearchForm({ onSearch }) {
    const [query, setQuery] = useState('');

    // BUG: query is always '' inside this callback because
    // the empty dependency array means it never recaptures query.
    const handleSubmit = useCallback(() => {
      onSearch(query);
    }, []); // Missing dependency: query

    // FIX: include query in the dependency array
    const handleSubmit = useCallback(() => {
      onSearch(query);
    }, [query, onSearch]);
  }
  ```

  **Pitfall 3 -- Inline objects/arrays defeating React.memo**: Even if a child component is wrapped in `React.memo`, passing an object or array literal as a prop creates a new reference every render, so the shallow comparison always fails.

  ```javascript theme={null}
  // The style object is recreated every render -- memo is useless
  <MemoizedBox style={{ color: 'red' }} />

  // Fix: define outside the component or use useMemo
  const boxStyle = useMemo(() => ({ color: 'red' }), []);
  <MemoizedBox style={boxStyle} />
  ```

  **Pitfall 4 -- Forgetting that useCallback needs React.memo on the child**: `useCallback` alone does not prevent re-renders. It stabilizes a function reference so that a `React.memo`-wrapped child can skip re-rendering. Without `React.memo` on the child, `useCallback` has zero effect.
</Warning>

***

## 🎯 Practice Exercises

<Accordion title="Exercise 1: Optimize a Slow List">
  ```javascript theme={null}
  // Before: Slow component
  function ProductList({ products, onSelect }) {
    const [filter, setFilter] = useState('');

    // Recalculates on every keystroke
    const filtered = products.filter(p => 
      p.name.toLowerCase().includes(filter.toLowerCase())
    );

    return (
      <div>
        <input 
          value={filter} 
          onChange={e => setFilter(e.target.value)} 
        />
        {filtered.map(product => (
          <ProductCard 
            key={product.id}
            product={product}
            onSelect={() => onSelect(product)}
          />
        ))}
      </div>
    );
  }

  // After: Optimized
  const ProductCard = React.memo(function ProductCard({ product, onSelect }) {
    return (
      <div onClick={onSelect}>
        <h3>{product.name}</h3>
        <p>${product.price}</p>
      </div>
    );
  });

  function ProductList({ products, onSelect }) {
    const [filter, setFilter] = useState('');
    const debouncedFilter = useDebounce(filter, 300);

    const filtered = useMemo(() => 
      products.filter(p => 
        p.name.toLowerCase().includes(debouncedFilter.toLowerCase())
      ),
      [products, debouncedFilter]
    );

    const handleSelect = useCallback((product) => {
      onSelect(product);
    }, [onSelect]);

    return (
      <div>
        <input 
          value={filter} 
          onChange={e => setFilter(e.target.value)} 
        />
        {filtered.map(product => (
          <ProductCard 
            key={product.id}
            product={product}
            onSelect={() => handleSelect(product)}
          />
        ))}
      </div>
    );
  }
  ```
</Accordion>

***

## Summary

| Technique           | Use Case                                      |
| ------------------- | --------------------------------------------- |
| **React.memo**      | Prevent child re-renders when props unchanged |
| **useMemo**         | Cache expensive calculations                  |
| **useCallback**     | Cache function references                     |
| **React.lazy**      | Code split by route/component                 |
| **Virtualization**  | Long lists (100+ items)                       |
| **Debouncing**      | Search inputs, resize handlers                |
| **Lazy loading**    | Images below the fold                         |
| **Bundle analysis** | Identify large dependencies                   |
| **Web Vitals**      | Measure real user experience                  |

<Card title="Next Steps" icon="arrow-right">
  In the next chapter, you'll learn about **Authentication & Protected Routes** — securing your React applications!
</Card>

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="Explain the relationship between React.memo, useMemo, and useCallback. When do you need all three together, and when is each one alone insufficient?">
    **Strong Answer:**
    These three tools form an optimization chain, and using one without the others often provides zero benefit.

    `React.memo` wraps a component and skips re-rendering when its props have not changed (by shallow comparison). But it only works if the props are actually the same by reference. If the parent passes a new object or function reference every render, `React.memo` compares the old and new references, finds them different, and re-renders anyway.

    `useMemo` fixes the object reference problem: `const config = useMemo(() => ({ theme: 'dark' }), [])` returns the same object reference across renders as long as dependencies are stable.

    `useCallback` fixes the function reference problem: `const handleClick = useCallback(() => doSomething(id), [id])` returns the same function reference as long as `id` has not changed.

    When you need all three together: a parent renders a memoized child that receives both an object prop and a callback prop. Without `useMemo` on the object and `useCallback` on the callback, `React.memo` on the child is useless because it receives new references every render.

    When each alone is insufficient: `React.memo` alone fails when props contain unstable references. `useMemo` alone is pointless if the child component is not memoized. `useCallback` alone is pointless if the child is not wrapped in `React.memo`. The anti-pattern I see most often: developers wrap every function in `useCallback` "for performance" without memoizing the child component. The `useCallback` adds overhead with zero benefit.

    **Follow-up: How do you measure whether memoization is actually helping? What tools do you use?**

    React DevTools Profiler is the primary tool. Record an interaction, then examine the flame graph. Components highlighted in gray did not render (memoization worked). The "Why did this render?" feature tells you exactly which props changed, confirming whether your memoization is preventing re-renders.

    For precise measurement, use the `React.Profiler` component with an `onRender` callback that logs `actualDuration`. Compare before and after memoization across multiple interactions.

    The rule: if you cannot measure a difference, do not memoize. The overhead of `useMemo` and `useCallback` (dependency comparison, closure storage) is small but nonzero. For trivial computations, memoization is slower than recomputing.
  </Accordion>

  <Accordion title="You are profiling a React app and discover that a list of 500 items re-renders entirely when a single item changes. Walk through your optimization strategy.">
    **Strong Answer:**
    Step 1: Identify why every item re-renders. Open React DevTools Profiler, record the interaction, and check "Why did this render?" for the list items. Common causes: the parent re-renders and children are not memoized, or a new callback reference is passed as a prop on every render.

    Step 2: Extract the list item into its own component if it is not already. You cannot memoize inline JSX in a map callback.

    Step 3: Wrap the list item component in `React.memo`. This is the biggest single win -- now each item only re-renders when its own props change.

    Step 4: Stabilize callback props. If each item receives `onClick={() => handleSelect(item.id)}`, that arrow function is new on every render, defeating `React.memo`. Refactor so the item receives a stable `onSelect` via `useCallback` and the item ID as a separate prop, letting the item call `onSelect(id)` internally.

    Step 5: If the list data is derived (filtered, sorted), wrap the derivation in `useMemo` so the filtered array reference stays stable when inputs have not changed.

    Step 6: For very long lists (1000+ items), add virtualization with react-window or react-virtuoso. This caps rendered components at the visible window.

    Step 7: Verify with the Profiler that only the changed item re-renders. The flame graph should show 499 gray items and 1 highlighted item.

    **Follow-up: React.memo uses shallow comparison. What happens when list items receive nested objects as props?**

    Shallow comparison checks reference equality for each prop. If a prop is a nested object recreated every render, `React.memo` sees a new reference and re-renders even if all values are identical.

    Three solutions: First, stabilize the object with `useMemo` in the parent. Second, flatten the props -- pass `userName` and `userCity` as separate primitive props instead of a nested object. Primitives are compared by value. Third, provide a custom comparison function to `React.memo` that checks the relevant fields. The custom comparator is powerful but dangerous if you forget to compare a prop that affects rendering.
  </Accordion>

  <Accordion title="What are React Server Components and how do they change the performance optimization model?">
    **Strong Answer:**
    React Server Components (RSC) execute on the server and send their rendered output to the client as a serialized component tree. They never run in the browser -- their JavaScript is not included in the client bundle. This fundamentally changes the optimization model because the primary concern shifts from "minimize client-side re-renders" to "minimize what ships to the client."

    In the traditional SPA model, every component's code is bundled, downloaded, parsed, and executed in the browser. Optimization means reducing re-renders (memo, useMemo, useCallback) and reducing bundle size (code splitting, tree shaking).

    With RSC, a component that fetches data, processes it, and renders HTML can run entirely on the server. The client receives the rendered output with no JavaScript for that component. For a page that imports a 200KB markdown parser to render blog content, RSC eliminates that 200KB from the client bundle entirely.

    The new optimization model: make as many components as possible Server Components (the default in Next.js App Router). Only add "use client" when you need interactivity (state, effects, event handlers, browser APIs). The boundary between server and client components is where you optimize.

    The tradeoff: Server Components cannot use hooks, cannot manage state, and cannot respond to user events. They are purely for rendering. This forces a clean separation between data fetching (server) and interactivity (client).

    **Follow-up: How does streaming with Suspense boundaries improve perceived performance with Server Components?**

    Without streaming, the server processes the entire page, then sends the complete HTML in one response. The user sees a blank page until the slowest data fetch completes. With streaming, the server sends HTML for fast-to-render parts immediately and streams in slower parts as they become ready.

    Suspense boundaries mark the streaming cut points. A Server Component wrapped in Suspense sends the skeleton HTML immediately and streams the real content when the data fetch resolves. The browser progressively replaces skeletons with content without any JavaScript.

    For the user, a page that takes 3 seconds to fully load feels near-instant because the layout, navigation, and above-the-fold content appear in 200ms. Slower sections stream in later. Client Components within the stream are hydrated selectively as their code loads, so interactivity arrives progressively too.
  </Accordion>
</AccordionGroup>
