Le Do Nghiem

Le Do Nghiem

AI Engineer

About meBooksSnippetsBlog

© 2026 Le Do Nghiem. All rights reserved.

Contact |

Back to Blog

React Performance Optimization Guide

Le Do Nghiem
Le Do NghiemAI Engineer
2025-09-09 4 min read
Share

Measure before you memo

I used to wrap every component in React.memo because someone on Twitter said it was "free performance." It was not free. It was harder-to-read code and sometimes slower renders from shallow compare overhead.

This guide is what I do now: Profiler first, optimize the hot path, ignore the rest. Pair with React 19 features when you are choosing patterns for forms and optimistic UI.


Step 0 — Measure

What: React DevTools Profiler — record an interaction, see what re-rendered and how long it took.

Why: Guessing wastes time. One heavy parent re-rendering fifty children beats one unmemoized leaf.

Red flag: Optimizing before you know the bottleneck. Seriously. Profile.


React.memo — when it helps

What: Skip re-render if props are shallow-equal to last time.

import React, { memo, useState } from "react";

const ExpensiveList = memo(function ExpensiveList({ items }) {
  console.log("ExpensiveList render");
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.label}</li>
      ))}
    </ul>
  );
});

export default function App() {
  const [count, setCount] = useState(0);
  const items = [{ id: 1, label: "Static" }];

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
      <ExpensiveList items={items} />
    </div>
  );
}

When I use it: Expensive child, stable props, parent re-renders often for unrelated state.

When I skip it: Cheap components, props that change every render anyway, premature optimization.


useCallback — stable function references

What: Return the same function identity until dependencies change.

import React, { useState, useCallback, memo } from "react";

const Child = memo(function Child({ onClick }) {
  console.log("Child render");
  return <button onClick={onClick}>Click</button>;
});

export default function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []);

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>Inc: {count}</button>
      <Child onClick={handleClick} />
    </div>
  );
}

Why: Without useCallback, Child gets a new onClick every parent render → memo useless.

Red flag: useCallback on every handler "just in case." Noise without a memoized child consuming it.


useMemo — expensive derived data

What: Cache a computed value between renders.

const sorted = useMemo(() => heavySort(items), [items]);

When: Sorting/filtering large lists, heavy formatting, referential equality for deps of other hooks.

When not: Trivial math — the memo cost can exceed the work.


Other wins (often bigger than memo)

  1. Virtualize long lists — react-window, TanStack Virtual
  2. Code-split routes — React.lazy + Suspense
  3. Move state down — do not lift state that only one branch needs
  4. Server Components (Next.js) — zero client JS for static trees. See use client vs use server
  5. Defer non-urgent updates — useDeferredValue, startTransition in React 18+

Reading the Profiler (quick)

After you record an interaction, look for:

  • Yellow bars — slow renders; click to see which component
  • "Why did this render?" — parent re-render vs props change
  • Commit duration — compare before and after your fix

I screenshot the before state. Future me forgets what "felt slow."


Memo mistakes I made

  • Memoing entire trees — fixed one child, parent still re-rendered everything below.
  • New object props — style={{ color: 'red' }} and inline {} defeat memo every time.
  • Profiling in dev only — production builds matter; check both when possible.

Your next profiling session

Open Profiler. Click the slow interaction. Fix the top offender — usually state placement or a missing key, not missing memo.

React performance is mostly architecture and data flow. memo, useCallback, and useMemo are precision tools for proven hot paths. Use them with evidence, not superstition.

On this page

  • Measure before you memo
  • Step 0 — Measure
  • React.memo — when it helps
  • useCallback — stable function references
  • useMemo — expensive derived data
  • Other wins (often bigger than memo)
  • Reading the Profiler (quick)
  • Memo mistakes I made
  • Your next profiling session
Share
Previous Post

Optimizing Next.js Apps with AI-Powered Image Processing

Next Post

Mastering TypeScript: Tips and Tricks