DNA⚛ïļ ReactPerformance Optimization
ðŸĢHatchlingReactPerformanceMemoizationVirtualizationRe-renders

Performance Optimization

React.memo, useMemo, useCallback, virtualization — learn when to optimize and more importantly, when NOT to.

Performance Optimization

React is fast by default. But as your app grows — more components, bigger lists, heavier computations — you'll start noticing slowdowns. This chapter teaches you how to find and fix performance problems, and equally important, when to leave things alone.


Why Does React Re-render?

Before you can fix unnecessary re-renders, you need to understand what triggers them. There are exactly three reasons a component re-renders:

  1. Its own state changes — you called setState or dispatch
  2. Its parent re-renders — even if the child's props didn't change
  3. A context it consumes changes — any component using useContext re-renders when the context value changes
function Parent() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>Parent count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Child name="Alice" />
    </div>
  );
}
 
function Child({ name }) {
  console.log('Child rendered!');
  return <p>Hello, {name}</p>;
}

Every time you click the button, Child re-renders — even though name never changes. Why? Because its parent re-rendered, and by default, all children re-render too.

Think of it like: When a manager calls an all-hands meeting (parent re-renders), every team member has to attend (children re-render), even if the meeting has nothing to do with them.


How to Prevent Unnecessary Re-renders

The first and best strategy is structural — organize your components so re-renders don't cascade:

Strategy 1: Move State Down

If only one part of the UI needs a piece of state, keep that state in the smallest possible component:

// Bad: Search state lives too high
function App() {
  const [search, setSearch] = useState('');
  return (
    <div>
      <SearchBar value={search} onChange={setSearch} />
      <ExpensiveProductList />
    </div>
  );
}
 
// Good: Search state lives where it's used
function App() {
  return (
    <div>
      <SearchSection />
      <ExpensiveProductList />
    </div>
  );
}
 
function SearchSection() {
  const [search, setSearch] = useState('');
  return <SearchBar value={search} onChange={setSearch} />;
}

Now when the user types, only SearchSection re-renders. ExpensiveProductList is untouched.

Strategy 2: Lift Content Up (Children Pattern)

Pass expensive components as children so they don't re-render when the parent's state changes:

function ScrollTracker({ children }) {
  const [scrollY, setScrollY] = useState(0);
 
  useEffect(() => {
    const handler = () => setScrollY(window.scrollY);
    window.addEventListener('scroll', handler);
    return () => window.removeEventListener('scroll', handler);
  }, []);
 
  return (
    <div>
      <p>Scroll position: {scrollY}</p>
      {children}
    </div>
  );
}
 
// Usage — ExpensiveList doesn't re-render on scroll!
function Page() {
  return (
    <ScrollTracker>
      <ExpensiveList />
    </ScrollTracker>
  );
}

The children JSX is created by the parent of ScrollTracker (which is Page). Since Page doesn't re-render on scroll, the children reference stays the same, and ExpensiveList skips re-rendering.

Remember: Before reaching for React.memo, try reorganizing your component tree. Moving state down and lifting content up solve most performance problems with zero overhead.


React.memo — The "Skip Re-render" Wrapper

React.memo wraps a component and tells React: "Only re-render this if its props actually changed."

const UserCard = React.memo(function UserCard({ name, email }) {
  console.log('UserCard rendered');
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
});

Now even if the parent re-renders, UserCard will skip re-rendering as long as name and email are the same.

When to Use React.memo

  • The component's parent re-renders frequently
  • The component is expensive to render (large list, heavy computations)
  • The component's props don't change often

When NOT to Use React.memo

  • The component is cheap to render (a simple <p> or <span>) — the memo comparison itself costs more than just re-rendering
  • The component always receives new props — memo checks on every render but always fails, adding overhead for nothing
  • The component's parent rarely re-renders — there's nothing to prevent

Common Mistake: Wrapping everything in React.memo. Memoization isn't free — React has to compare all props on every render. For simple components, this comparison is slower than just re-rendering.

Interview Q&A:

Q: Does React.memo do a deep comparison of props?

No, it does a shallow comparison by default. It checks if each prop is the same reference using ===. This means { color: 'red' } !== { color: 'red' } because they're different objects in memory. You can pass a custom comparison function as the second argument, but this is rarely needed.


useMemo — Caching Expensive Calculations

useMemo lets you cache the result of a calculation so it only re-runs when its dependencies change.

Think of it like a recipe with leftovers. If you cooked lasagna yesterday and nobody ate it, you don't cook a fresh one today — you serve the leftovers. You only cook again when the ingredients change or the leftovers run out.

function FilteredTodoList({ todos, filter }) {
  const filteredTodos = useMemo(() => {
    return todos.filter(todo => {
      if (filter === 'active') return !todo.completed;
      if (filter === 'completed') return todo.completed;
      return true;
    });
  }, [todos, filter]);
 
  return (
    <ul>
      {filteredTodos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Without useMemo, the filter() runs on every re-render — even if todos and filter haven't changed. With useMemo, React says: "Same inputs? Here are the cached results."

When to Use useMemo

  • The calculation is genuinely expensive (filtering thousands of items, complex transformations)
  • You need a stable reference to an object or array (for passing to a memoized child component)
// Stable reference for a memoized child
function Parent() {
  const [count, setCount] = useState(0);
 
  const config = useMemo(() => ({
    theme: 'dark',
    size: 'large',
  }), []);
 
  return <MemoizedChild config={config} />;
}

useCallback — Keeping Function References Stable

useCallback is useMemo for functions. It returns the same function reference unless its dependencies change.

Why does this matter? Every time a component renders, it creates new function instances:

function Parent() {
  const [count, setCount] = useState(0);
 
  const handleClick = () => console.log('clicked');
 
  return <MemoizedChild onClick={handleClick} />;
}

Even though handleClick does the same thing every time, it's a brand new function object on each render. So React.memo on MemoizedChild fails — it sees onClick as "changed."

The fix:

function Parent() {
  const [count, setCount] = useState(0);
 
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);
 
  return <MemoizedChild onClick={handleClick} />;
}

Now handleClick is the same reference across renders, and React.memo works correctly.

Think of it like: Imagine giving someone your phone number. If you write it on a new piece of paper every time, they'd think it's a different number. useCallback is like giving them the same business card every time — same number, same card.


What Is Memoization?

Memoization is a fancy word for a simple idea: remember the result of a computation so you don't have to redo it.

Think of it like a kitchen shortcut. If a customer orders the same dish you just made, you don't start cooking from scratch — you serve from what you already prepared. You only cook fresh when a new, different order comes in.

In React, memoization shows up in three places:

ToolWhat It MemoizesPurpose
React.memoA component's rendered outputSkip re-rendering if props are the same
useMemoA computed valueSkip recalculating if dependencies are the same
useCallbackA function referenceKeep the same function identity across renders

They all follow the same pattern:

  1. First time: Do the work, store the result
  2. Next time, same inputs: Return the stored result
  3. Next time, different inputs: Redo the work, store the new result

When NOT to Memoize — The Overhead Trap

Memoization has a cost:

  • Memory — React stores the previous result
  • Comparison — React compares dependencies on every render
  • Complexity — More code to read and maintain
// Don't do this — the memo costs more than the computation
const greeting = useMemo(() => `Hello, ${name}!`, [name]);
 
// Just compute it directly
const greeting = `Hello, ${name}!`;

The rule of thumb: Don't memoize unless you've measured a performance problem. Premature memoization adds complexity for zero benefit.

// Don't do this — wrapping every component in React.memo
const Title = React.memo(({ text }) => <h1>{text}</h1>);
 
// Just render it — it's a single DOM node, blazing fast already
const Title = ({ text }) => <h1>{text}</h1>;

Think of it like: Putting every single item in your fridge in a separate labeled container. Sure, it's organized, but the time spent labeling and finding containers is more than the time you save. Only containerize the expensive stuff.

When memoization is worth it:

  • Filtering/sorting thousands of items
  • Components that render large subtrees (tables, charts, dashboards)
  • Functions passed as props to memoized children

When memoization is NOT worth it:

  • Simple string concatenation or math
  • Components that render a few DOM nodes
  • Components whose parent rarely re-renders anyway

Remember: Profile first, optimize second. React DevTools has a Profiler tab that shows which components rendered and how long they took. Use it before adding any memoization.


Virtualization — Only Render What's Visible

Imagine a restaurant menu with 10,000 dishes. You wouldn't print all 10,000 on one giant scroll — you'd show one page at a time. That's virtualization.

When you have a long list (hundreds or thousands of items), rendering every single item into the DOM is expensive — even if the user can only see 10 at a time. Virtualization means you only create DOM nodes for the items currently visible on screen.

import { FixedSizeList } from 'react-window';
 
function VirtualTodoList({ todos }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {todos[index].text}
    </div>
  );
 
  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={todos.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

Even with 10,000 todos, only about 8-10 <div> elements exist in the DOM at any time. As the user scrolls, react-window creates new DOM nodes for items coming into view and removes nodes for items leaving view.

When to Virtualize

List SizeAction
Under 100 itemsDon't bother — just render them all
100–500 itemsConsider it if items are complex (images, nested components)
500+ itemsDefinitely virtualize
10,000+ itemsVirtualization is essential

Virtualization + Infinite Scroll

Virtualization pairs naturally with infinite scroll — load more data as the user scrolls near the bottom:

import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
 
function InfiniteUserList({ users, hasMore, loadMore }) {
  const isItemLoaded = (index) => index < users.length;
 
  return (
    <InfiniteLoader
      isItemLoaded={isItemLoaded}
      itemCount={hasMore ? users.length + 1 : users.length}
      loadMoreItems={loadMore}
    >
      {({ onItemsRendered, ref }) => (
        <FixedSizeList
          height={500}
          itemCount={hasMore ? users.length + 1 : users.length}
          itemSize={60}
          onItemsRendered={onItemsRendered}
          ref={ref}
        >
          {({ index, style }) => (
            <div style={style}>
              {isItemLoaded(index) ? (
                <UserRow user={users[index]} />
              ) : (
                <p>Loading...</p>
              )}
            </div>
          )}
        </FixedSizeList>
      )}
    </InfiniteLoader>
  );
}

The user sees a smooth, scrollable list. Behind the scenes, only visible items are in the DOM, and new data loads automatically as they scroll.

Common Mistake: Trying to optimize a list with React.memo on each item when you actually need virtualization. If you have 5,000 items, memo helps each item re-render faster, but virtualization means you only render ~10 items total. That's a much bigger win.


Performance Optimization Checklist

Before you optimize anything, ask these questions in order:

  1. Is there actually a problem? Use React DevTools Profiler to measure
  2. Can I move state down? Keep state close to where it's used
  3. Can I lift content up? Pass expensive components as children
  4. Is a long list the bottleneck? Use virtualization
  5. Is an expensive calculation repeated? Use useMemo
  6. Is a memoized child re-rendering due to function props? Use useCallback
  7. Is a large subtree re-rendering due to parent? Use React.memo

Remember: The best optimization is the one you don't need to write. Good component architecture prevents most performance problems before they start.


Chapter Summary

ToolWhat It DoesAnalogy
Move state downPrevents cascading re-rendersKeep meetings small — don't invite the whole company
React.memoSkips re-render if props unchanged"Skip this meeting if nothing relevant changed"
useMemoCaches expensive computation resultsDon't re-cook the same dish — serve leftovers
useCallbackKeeps function reference stableSame business card, not a new one each time
VirtualizationOnly renders visible list itemsShow one menu page, not all 10,000 dishes