Back to blog

React Performance Optimization: Practical Guide using useMemo, useCallback, and React.memo

React's reactive model makes building dynamic user interfaces straightforward. However, as applications grow complex, developers often encounter performance degradation—sluggish inputs, laggy animations, or delayed page transitions.

This slowness is usually caused by unnecessary re-renders and expensive computations blocking the main thread.

In this guide, we will analyze React's rendering lifecycle, explore how to isolate bottleneck nodes, and learn how to use React.memo, useMemo, and useCallback to optimize your code.

Understanding React's Render Loop

React updates pages in two phases:

  1. Render Phase: React executes the component function, computes a new Virtual DOM tree, and compares it with the previous Virtual DOM tree (a process called Reconciliation).
  2. Commit Phase: React applies the calculated differences directly to the physical browser DOM.

By default, if a parent component renders, all its child components render recursively, regardless of whether their props have changed. If your page tree is deep, minor state changes near the root can trigger hundreds of child renders, slowing down user interaction.

Optimization 1: React.memo (Skipping Child Renders)

To prevent a child component from re-rendering when its parent updates, wrap the child component in React.memo.

React.memo is a higher-order component that performs a shallow comparison of the component's incoming props against its previous props. If the props are identical, React skips the rendering phase for that component.

import React from 'react';

interface UserCardProps {
  name: string;
}

// Wrapped in React.memo
export const UserCard = React.memo(function UserCard({ name }: UserCardProps) {
  console.log('UserCard rendered');
  return <div class="border p-4">{name}</div>;
});

If the parent component re-renders but the name prop has not changed, UserCard will not execute, saving CPU cycles.

Optimization 2: useCallback (Caching Function References)

React.memo only works if props remain identical. In JavaScript, functions are objects, which means they are compared by reference.

If you pass a function to a memoized child component:

// Inside Parent Component
const handleClick = () => {
  console.log('Clicked');
};

return <UserCard name="Alex" onClick={handleClick} />;

Every time the parent renders, JavaScript creates a new function reference for handleClick. Because the reference changes, the shallow comparison in UserCard fails, forcing it to render anyway.

To fix this, wrap the function inside the useCallback hook. This caches the function reference across renders:

import { useCallback } from 'react';

// Caches the function reference
const handleClick = useCallback(() => {
  console.log('Clicked');
}, []); // Empty dependencies array: reference never changes

Now, UserCard receives the exact same function reference on every render, allowing React.memo to skip updates.

Optimization 3: useMemo (Caching Expensive Calculations)

If a component performs heavy calculations (such as filtering, sorting, or mapping thousands of objects), executing that code on every render blocks the event loop.

Wrap the expensive logic inside the useMemo hook to cache the calculated result:

import { useMemo } from 'react';

const sortedList = useMemo(() => {
  console.log('Sorting list...');
  return largeDataset.sort((a, b) => b.score - a.score);
}, [largeDataset]); // Only re-runs the sort function if largeDataset changes

If other state variables (like dark mode toggles) trigger a component render, React skips the sort logic, returning the cached list instantly.

Summary Checklist for Hook Optimization

Tool Purpose What it Caches When to Use
React.memo Prevents child component re-renders Component output For child components with props that rarely change
useCallback Maintains function reference integrity Function definition When passing function props to memoized child components
useMemo Prevents recalculation of values Return value of a function For expensive algorithms or object dependency caching

Optimization 4: List Virtualization

If your page renders thousands of database rows (such as infinite scroll tables), writing all those DOM nodes blocks browser engines.

Use List Virtualization (via libraries like react-window or react-virtualized). A virtualized list only renders the specific items currently visible inside the user's viewport, swapping elements dynamically as the user scrolls, keeping DOM node counts low.

Conclusion

Performance tuning is a balance. Do not wrap every component or function in optimization hooks, as hooks introduce memory and execution comparisons overhead. Instead, focus on wrapping components receiving frequent updates, memoize functions passed to child components, cache complex data transformations, and virtualize large tables to ensure a fast user experience.