React|Optimization & Security

useMemo, useCallback, React.memo: Memoization Is Not Free

2
useMemo, useCallback, React.memo: Memoization Is Not Free

Chris was deeply inspired after taking a React performance optimization course.

"If I memoize every variable and function, it won't have to recalculate anything, so it will definitely be faster, right?"

From that day on, Chris started plastering useMemo and useCallback all over his code.

// ❌ Worst Example: Meaningless Memoization
const one = useMemo(() => 1, []);
const plus = useCallback(() => console.log('plus'), []);

What was the result? The app actually became slower, and memory usage increased.

Memoization is not free. It is essentially "spending Memory (RAM) to buy Computation Time (CPU)." If the cost outweighs the benefit, it's better not to do it at all.

Today, we will establish clear criteria for when exactly React's memoization trio is needed and when it should be avoided.

1. useMemo: Remembering Values

useMemo caches the result of a heavy calculation. If the values in the dependency array (deps) haven't changed, it reuses the previously calculated value.

When should you use it?

If "heavy calculation" feels vague, think of it this way:

"Does this code take longer than 0.1ms to run?"

// ✅ Good Example: Filtering and sorting arrays (when data is large)
const sortedUsers = useMemo(() => {
  return users.filter(u => u.active).sort((a, b) => b.score - a.score);
}, [users]);

// ❌ Bad Example: Simple transformation
// The cost of setting up useMemo (object creation, comparison) is higher.
const userCount = useMemo(() => users.length, [users]);

The React official documentation advises, "There is no benefit to wrapping a calculation unless it involves looping over thousands of items."

2. React.memo: Remembering Components

React.memo is a Higher-Order Component (HOC) that skips re-rendering if the Props haven't changed.

// Child.tsx
const Child = React.memo(({ name, onClick }: { name: string, onClick: () => void }) => {
  console.log("Child Rendering!");
  return <div onClick={onClick}>{name}</div>;
});

However, there is a trap here. When the parent component re-renders, the functions defined inside the parent are also re-created.

  • Parent re-renders.
  • handleClick function is re-created (Reference address changes).
  • New onClick prop is passed to Child.
  • React.memo thinks, "Huh? Props changed."
  • Child re-render occurs (Optimization failed).
  • To solve this problem, useCallback was introduced.

    3. useCallback: Remembering Functions

    useCallback allows you to reuse an existing function (reference) instead of creating a new one.

    Referential Equality

    In JavaScript, {} === {} is false. Objects (including functions) are treated as different if their reference addresses differ, even if their content is the same.

    useCallback plays the role of fixing this reference address.

    // Parent.tsx
    function Parent() {
      const [count, setCount] = useState(0);
    
      // ✅ Wrap with useCallback to prevent Child's unnecessary re-render.
      const handleClick = useCallback(() => {
        console.log("Click");
      }, []); // With no dependencies, it maintains the same function reference forever.
    
      return (
        <div>
          <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
          {/* Now, even if count changes and Parent re-renders, Child stays quiet. */}
          <Child name="Chris" onClick={handleClick} />
        </div>
      );
    }

    Core Rule: useCallback is meaningless if used alone. It is only effective when the child component is wrapped in React.memo. There is no need to use it for functions passed to standard div or button elements.

    4. The Cost of Memoization

    Why shouldn't we memoize everything?

  • Memory Overhead: Cached values and functions remain in memory and are not Garbage Collected (GC).
  • Comparison Cost: React has to compare the values in the dependency array (deps) one by one on every render. This comparison cost can be higher than the simple calculation cost.
  • Code Complexity: Managing dependency arrays is the developer's responsibility. Missing even one dependency leads to bugs (Stale Closures).
  • 5. Practical Guidelines

    Chris has now set clear criteria.

    ⭕ When to Use

  • Maintaining Referential Equality: When passing an object or function to the dependency array of useEffect.
  • React.memo Optimization: When passing a function as a prop to a child component wrapped in React.memo.
  • Truly Heavy Calculations: Complex logic that takes more than 1ms to execute.
  • ❌ When NOT to Use

  • Simple Calculations: Operations like props.a + props.b.
  • Passing Functions to HTML Tags: Like <button onClick={handleChange}>. Changing browser DOM event handlers has almost no impact on performance.
  • Key Takeaways

  • useMemo: Caches the result of a value. Prevents heavy calculations.
  • useCallback: Caches the reference of a function. Used with React.memo to prevent child re-renders.
  • React.memo: Caches the render result of a component. Reuses it if Props are the same.
  • Principle: Optimization is not too late if applied after measuring when a problem arises. Do not Prematurely Optimize.

  • Now that we've taken care of performance, it's time to put the app on a diet. Is there any need to download the code for the 'Admin Page' that the user hasn't even entered yet?

    Let's learn about Code Splitting, the technique of splitting code into pieces and loading them only when needed.

    Continuing in: “Bundle Size Diet: Code Splitting Strategy Using React.lazy and Suspense"

    🔗 References

  • React Docs - useMemo
  • React Docs - useCallback
  • Kent C. Dodds - When to useMemo and useCallback