Skip to main content

Overview

Understand how React works internally to build better, more performant applications. Learn about the virtual DOM, reconciliation algorithm, when components re-render, and how to optimize performance.

Virtual DOM

React’s in-memory representation of UI

Reconciliation

How React updates the DOM efficiently

Component Re-rendering

When and why components update

Performance

Optimization techniques and tools

How React Updates the DOM

1

Component Renders

Component function executes, returning JSX
2

Virtual DOM Created

React creates virtual DOM representation from JSX
3

Reconciliation

React compares new virtual DOM with previous version
4

DOM Updates

Only changed elements are updated in real DOM
Why Virtual DOM?Direct DOM manipulation is slow. React’s virtual DOM:
  • Batches multiple updates together
  • Calculates minimum changes needed
  • Updates real DOM only once
  • Results in better performance

When Components Re-render

A component re-renders when:

State Changes

useState or useReducer state updates

Props Change

Parent passes different props

Context Changes

Context value updates

Parent Re-renders

Parent component re-renders by default
Default Behavior:When a parent component re-renders, all child components re-render by default, even if their props haven’t changed. This can impact performance in large component trees.

Preventing Unnecessary Re-renders with memo

Use React.memo() to prevent component re-renders when props haven’t changed.
CounterOutput.jsx
export default function CounterOutput({ value }) {
  // Re-renders every time parent renders
  const cssClass = value >= 0 ? 'counter-output' : 'counter-output negative';
  return <span className={cssClass}>{value}</span>;
}
This component re-renders even if value is the same.

Complete Example

Counter.jsx
import { useState, memo } from 'react';
import CounterOutput from './CounterOutput.jsx';

function isPrime(number) {
  if (number <= 1) return false;
  
  const limit = Math.sqrt(number);
  for (let i = 2; i <= limit; i++) {
    if (number % i === 0) return false;
  }
  return true;
}

const Counter = memo(function Counter({ initialCount }) {
  const initialCountIsPrime = isPrime(initialCount);
  const [counter, setCounter] = useState(initialCount);

  function handleDecrement() {
    setCounter((prevCounter) => prevCounter - 1);
  }

  function handleIncrement() {
    setCounter((prevCounter) => prevCounter + 1);
  }

  return (
    <section className="counter">
      <p className="counter-info">
        The initial counter value was <strong>{initialCount}</strong>. It{' '}
        <strong>is {initialCountIsPrime ? 'a' : 'not a'}</strong> prime number.
      </p>
      <p>
        <button onClick={handleDecrement}>Decrement</button>
        <CounterOutput value={counter} />
        <button onClick={handleIncrement}>Increment</button>
      </p>
    </section>
  );
});

export default Counter;
memo performs a shallow comparison of props:
  • If props are the same, skip re-render
  • If props changed, re-render component
  • Compares using Object.is() by default
Use memo when:
  • Component renders often with same props
  • Component is expensive to render
  • Component is pure (same props = same output)
Don’t overuse - memo itself has cost.

The useMemo Hook

Memoize expensive calculations so they only re-run when dependencies change.
const Counter = memo(function Counter({ initialCount }) {
  // isPrime runs on EVERY render
  const initialCountIsPrime = isPrime(initialCount);
  const [counter, setCounter] = useState(initialCount);
  
  // Each time counter changes, isPrime recalculates
  // even though initialCount hasn't changed!
  
  return (/* ... */);
});

useMemo Syntax

const memoizedValue = useMemo(
  () => {
    // Expensive calculation
    return computeExpensiveValue(a, b);
  },
  [a, b] // Dependencies
);
When to Use useMemo:
  • Expensive calculations (complex algorithms)
  • Creating objects/arrays passed as props to memoized children
  • Referential equality matters for child components
When NOT to Use:
  • Simple calculations (overhead not worth it)
  • Values that change frequently anyway
  • As premature optimization

The useCallback Hook

Memoize function references to prevent child re-renders.
Counter.jsx
import { useState, memo, useCallback, useMemo } from 'react';

const Counter = memo(function Counter({ initialCount }) {
  const initialCountIsPrime = useMemo(
    () => isPrime(initialCount),
    [initialCount]
  );

  const [counter, setCounter] = useState(initialCount);

  // Functions are memoized - same reference unless deps change
  const handleDecrement = useCallback(function handleDecrement() {
    setCounter((prevCounter) => prevCounter - 1);
  }, []);

  const handleIncrement = useCallback(function handleIncrement() {
    setCounter((prevCounter) => prevCounter + 1);
  }, []);

  return (
    <section className="counter">
      <p className="counter-info">
        The initial counter value was <strong>{initialCount}</strong>. It{' '}
        <strong>is {initialCountIsPrime ? 'a' : 'not a'}</strong> prime number.
      </p>
      <p>
        <IconButton icon={MinusIcon} onClick={handleDecrement}>
          Decrement
        </IconButton>
        <CounterOutput value={counter} />
        <IconButton icon={PlusIcon} onClick={handleIncrement}>
          Increment
        </IconButton>
      </p>
    </section>
  );
});
// useMemo - memoizes the RESULT
const value = useMemo(() => computeValue(a, b), [a, b]);

// useCallback - memoizes the FUNCTION
const callback = useCallback(() => doSomething(a, b), [a, b]);

// These are equivalent:
const callback = useCallback(fn, deps);
const callback = useMemo(() => fn, deps);

Keys and Component Identity

React uses keys to identify which elements changed, were added, or removed.
{items.map((item) => (
  <ListItem key={item.id} data={item} />
))}
Keys help React:
  • Identify which items changed
  • Preserve component state
  • Optimize re-renders
Changing a component’s key forces React to unmount and remount it:
<Counter key={chosenCount} initialCount={chosenCount} />
When chosenCount changes, React:
  1. Unmounts old Counter
  2. Mounts new Counter with fresh state
  3. Useful for resetting component state
Key Anti-patterns:
// ❌ Don't use array index as key
{items.map((item, index) => <Item key={index} />)}

// ❌ Don't use random values
{items.map((item) => <Item key={Math.random()} />)}

// ✅ Use stable unique identifiers
{items.map((item) => <Item key={item.id} />)}

React’s Rendering Process

1

Trigger

State update, prop change, or context change triggers render
2

Render Phase

React calls component functions to get new virtual DOM
3

Reconciliation

React compares new virtual DOM with previous snapshot
4

Commit Phase

React updates actual DOM with minimum changes needed
5

Browser Paint

Browser paints updated DOM to screen
Important Distinction:
  • Rendering = Calling component function
  • Committing = Updating the DOM
A component can render without DOM changes if output is identical.

Performance Optimization Checklist

Use memo

Prevent re-renders when props unchanged

Use useMemo

Memoize expensive calculations

Use useCallback

Memoize function references

Proper Keys

Use stable unique keys in lists

Code Splitting

Lazy load components not immediately needed

Virtualization

Render only visible list items
Optimization Philosophy:
  1. Profile first - Don’t optimize prematurely
  2. Measure impact - Use React DevTools Profiler
  3. Focus on bottlenecks - Optimize what matters
  4. Keep it simple - Readable code > clever optimizations

Million.js Integration

The course also covers Million.js, a compiler that optimizes React applications:
import { block } from 'million/react';

// Wrap components for automatic optimization
const Counter = block(function Counter({ initialCount }) {
  // Component code
});
Million.js analyzes your components and applies optimizations automatically, potentially improving performance without manual memoization.

Common Performance Pitfalls

// ❌ New object every render
<Child style={{ color: 'red' }} />

// ✅ Stable reference
const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />
// ❌ New function every render
<Child onClick={() => doSomething()} />

// ✅ Memoized function
const handleClick = useCallback(() => doSomething(), []);
<Child onClick={handleClick} />
// ❌ Runs every render
const expensiveResult = computeExpensiveValue(data);

// ✅ Memoized
const expensiveResult = useMemo(
  () => computeExpensiveValue(data),
  [data]
);

Profiling and Debugging

Use React DevTools Profiler to:
  • Record component render times
  • Identify unnecessary re-renders
  • Find performance bottlenecks
  • Visualize component updates

Resources

React DevTools

Profile and debug React applications

React Reconciliation

Deep dive into React’s reconciliation

Build docs developers (and LLMs) love