React Memoization: A Complete Guide to Performance Optimization

React Memoization

Developing a fast and responsive web application is not just about adding features. It also depends on how efficiently the application handles its tasks. As React applications grow in complexity, components can re-render unnecessarily, and complex calculations may require extra processing and affect performance. These small inefficiencies can eventually affect loading speed, responsiveness, and the overall user experience.

This is where React memoization becomes useful. It provides a way for ReactJS development companies to avoid repeating work when the required data or inputs have not changed. This can be particularly helpful in applications with complex interfaces, frequent updates, or resource-intensive operations. However, memoization is not something that every component automatically needs. Using it in the right situations can improve efficiency without adding unnecessary complexity to the codebase.

In this article, we will explore React memoization, how it works, the main techniques available, its advantages, and usage scenarios.

What is Memoization?

Memoization is a useful optimization technique for making programs run more efficiently by remembering results that have already been calculated. When a function receives the same input again, the program can use the cached result instead of performing the same work from the beginning. This can save processing time and reduce unnecessary calculations, especially in the case of expensive operations that are repeated frequently. In React, memoization can also help improve application performance by preventing components from doing unnecessary work when their inputs have not changed.

What Are the Benefits of Memoization?

The following are some of the most visible advantages of implementing memoization: 

  • Improved Performance: Memoization can greatly improve program speed by reusing results from earlier calculations. This is especially helpful when a function performs complex, time-consuming, or recursive operations, as the program can avoid doing the same work again.
  • Reduced Redundancy: Memoization helps a program work more efficiently by caching previously calculated results. When the same calculation is needed again, the stored answer can be reused instead of processing it from scratch. This reduces unnecessary CPU work and can make applications respond faster.
  • Increased Efficiency: Memoization reuses results that are already available, which reduces the amount of processing required. This allows applications to use resources more wisely, respond more quickly, and maintain smoother performance during complex tasks.
  • Scalability: Memoization can support scalable application development by reducing repeated work in frequently used functions. When the same inputs occur often, saved results can be reused, which allows your system to handle more data and user requests without unnecessary processing.
  • Resource Optimization: Memoization can reduce unnecessary work by reusing previously generated results instead of repeatedly requesting the same information. This reduces the processing work, so applications can use available computing resources more efficiently.

When to Use Memoization?

The following are the best usage scenarios where memoization can give the best expected outcomes: 

  • Expensive Calculations: If any component performs complex or repeated calculations, such as sorting, filtering, or formatting. If the inputs stay unchanged, cached results can prevent unnecessary processing and improve performance.
  • Callbacks: When functions are passed to child components that are memoized or deeply nested. Memoizing callbacks prevents unnecessary child re-renders and improves application performance.
  • Pure Functions: When you’re using pure functions and components whose results depend only on their inputs and have no side effects.
  • Props: If a component’s props remain unchanged for quite a long period, memoization allows React to reuse the existing result instead of re-computing output and unnecessary re-rendering.

How to Implement Memoization in Functional Components in React?

In React, you can selectively memoize components using the built-in mechanisms, with each one serving a different optimization purpose. The three popular built-in tools are as follows:

React.memo()

React.memo() is a useful way to reduce unnecessary rendering in functional components. This higher-order component compares a component’s current props with its previous ones and can skip rendering when there is no change. This is especially helpful when a parent updates frequently but its child components receive the same props and do not need to update.

useMemo()

The useMemo() hook helps React avoid repeating expensive calculations during every render. It stores the value produced by a calculation between renders and checks its dependencies before running it again. When those dependencies stay unchanged, React can reuse the stored value. This can make components more efficient when they perform complex or time-consuming operations.

useCallback()

The useCallback() hook helps React keep the same function reference between component renders until its dependencies change. This can be useful when functions are passed as props to child components, especially if those children use memoization. It takes a function to be memoized and a dependency array as arguments for rendering. After the first render, React recreates the function only when any of the dependencies in the dependency array changes between renders.

Let’s quickly compare these three memoization techniques to gain better conceptual clarity:

TechniqueUse CaseBenefit
React.memoWraps componentsPrevents re-rendering of functional components
useMemoStores results of expensive computationsReduces CPU time by reusing calculations
useCallbackMemoizes function definitionsPrevents recreating functions on every render

How to Implement Memoization in React?

Step 1: Set Up Your Project 

Create a new React application using Vite with the command shown below:

Then navigate into the project folder with the command shown below:

cd react-performance-lab 
npm install

Step 2: Organize Your Project Structure 

Create the following folders and files inside your src directory:

src/ 
├── components/ 
 │   └── Child.tsx 
 │   └── PureChild.tsx 
├── App.tsx 
├── main.tsx 
 └── styles.css

Here’s what each file does: 

  • components/Child.tsx: A memoized component used to demonstrate and test unnecessary re-renders.
  • App.tsx: The main component where state is managed and React performance behavior is tested.
  • main.tsx: The application entry point that renders the React app. For this example, it is configured without StrictMode.
  • styles.css: Contains the application styles and CSS.

Step 3: Add Your Code Files 

We have four files to create. Copy each one exactly as shown below.

File 1: Create src/components/Child.tsx (React.memo) 

This component is wrapped with React.memo, which helps prevent unnecessary re-renders. If the parent component re-renders but the props remain the same, this component will not re-render. 

Create a new folder named components inside the src directory, and then create a Child.tsx file inside that folder.

import React from "react"; 
 
interface ChildProps { 
  count: number; 
  onIncrement: () => void; 
} 
 
export const Child = React.memo(({ count, onIncrement }: ChildProps) => { 
  console.log("Child component rendered"); 
 
  return ( 
	<div className="child-box memoized"> 
  	<h4 className="child-box-title">Child Component (React.memo)</h4> 
  	<p className="child-box-description">Parent Count: {count}</p> 
  	<p className="child-box-description"> 
    	This component only re-renders if its props change. 
  	</p> 
  	<button className="btn btn-secondary w-full" onClick={onIncrement}> 
    	Increment Parent Counter 
  	</button> 
	</div> 
  ); 
});

Key Point: The console.log statement helps you track when the component renders. By checking the browser console, you can verify whether the component is re-rendering. This makes it easy to confirm that React.memo is preventing unnecessary re-renders.

File 2: Create src/components/ PureChild.tsx (React.PureComponent) 

This component uses React.PureComponent, which is the class component equivalent of React.memo. It helps prevent unnecessary re-renders by performing a shallow comparison of props and state before rendering. 

If the parent component re-renders but the props and state remain unchanged, React.PureComponent skips the render process and reuses the existing output. 

Create a new file named PureChild.tsx inside the components folder and add the following code:

import React from "react"; 
 
interface PureChildProps { 
  count: number; 
  onIncrement: () => void; 
} 
 
export class PureChild extends React.PureComponent<PureChildProps> { 
  render() { 
    console.log("PureChild component rendered"); 
 
	return ( 
  	<div className="child-box memoized"> 
    	<h4 className="child-box-title">Pure Component (Class)</h4> 
    	<p className="child-box-description">Parent Count: {this.props.count}</p> 
    	<p className="child-box-description"> 
          This uses React.PureComponent for automatic shallow prop comparison. 
    	</p> 
    	<button 
          className="btn btn-secondary w-full" 
          onClick={this.props.onIncrement} 
    	> 
      	Increment from PureComponent 
    	</button> 
  	</div> 
	); 
  } 
}

The console.log statement helps track when the PureChild component renders. By checking the browser console, you can easily verify that React.PureComponent prevents unnecessary re-renders when props and state remain unchanged. 

File 3: Create src/App.tsx (useMemo & useCallback) 

This is the main application component. It uses useState to manage state, useMemo to optimize product filtering, and useCallback to prevent unnecessary function recreation. 

Update the App.tsx file with the following code:

import { useState, useMemo, useCallback } from "react"; 
import { Child } from "./components/Child"; 
import { PureChild } from "./components/PureChild"; 
 
export default function App() { 
  const [count, setCount] = useState(0); 
  const [search, setSearch] = useState(""); 
  const [active, setActive] = useState(false); 
 
  const products = [ 
	"Laptop", 
	"Phone", 
	"Mouse", 
	"Keyboard", 
	"Monitor", 
	"Headphones", 
  ]; 
 
  const filteredProducts = useMemo(() => { 
    console.log("Filtering products list"); 
	return products.filter((item) => 
      item.toLowerCase().includes(search.toLowerCase()), 
	); 
  }, [search]); 
 
  const handleIncrement = useCallback(() => { 
    setCount((prev) => prev + 1); 
  }, []); 
 
  return ( 
	<div className="app-container app-wrapper"> 
  	<section className="lab-card"> 
    	<div className="flex-between"> 
      	<h3 className="lab-title lab-title-m0">Performance Dashboard</h3> 
      	<button 
            className={`btn ${active ? "btn-accent" : "btn-secondary"}`} 
            onClick={() => setActive(!active)} 
      	> 
        	Trigger Parent State ({active ? "Active" : "Inactive"}) 
      	</button> 
    	</div> 
 
    	<hr className="divider" /> 
 
    	<div className="form-group"> 
      	<label className="form-label" htmlFor="search-input"> 
        	Filter Products (useMemo) 
      	</label> 
      	<input 
        	type="text" 
        	id="search-input" 
        	placeholder="Search products..." 
        	value={search} 
            onChange={(e) => setSearch(e.target.value)} 
            className="form-input" 
      	/> 
    	</div> 
 
    	<div className="child-box child-box-results"> 
      	<h4 className="text-secondary-small">Filtered Results:</h4> 
      	<ul className="list-padding-small"> 
        	{filteredProducts.length === 0 ? ( 
          	<li className="no-results">No products match your search.</li> 
        	) : ( 
              filteredProducts.map((product) => ( 
            	<li key={product} className="list-item-spacing"> 
              	{product} 
            	</li> 
          	)) 
        	)} 
      	</ul> 
    	</div> 
 
    	<hr className="divider" /> 
 
    	<div className="form-group"> 
      	<label className="form-label"> 
        	Prop Reference Test (useCallback) 
      	</label> 
      	<p className="text-counter-label"> 
        	Parent Counter: <strong className="accent-text">{count}</strong> 
      	</p> 
      	<div className="flex-col-gap"> 
        	<Child count={count} onIncrement={handleIncrement} /> 
        	<PureChild count={count} onIncrement={handleIncrement} /> 
      	</div> 
    	</div> 
  	</section> 
	</div> 
  ); 
}

File 4: Update src/main.tsx 

This is the entry point of the application. For this example, StrictMode is not used so that component render behavior can be observed more clearly during testing. 

Update the main.tsx file with the following code:

import { createRoot } 
  from 'react-dom/client' 
import './styles.css' 
import App from './App.tsx' 
 
createRoot( 
  document.getElementById('root')! 
).render( 
  <App /> 
)

Step 4: Run Your Project

Run the following command in your terminal to start the application.

npm run dev

Once the development server starts, open your browser to view the application. 

How to Test?

Now let’s verify that our optimization techniques are working correctly.

  1. Open the Browser Console
    Press F12 (or Ctrl + Shift + I on Windows, Cmd + Option + I on Mac) to open the browser’s developer tools and navigate to the Console tab.
  2. Click the Toggle Button
    Click the “Trigger Parent State” button multiple times and observe the console output.
  3. Verify the Result
    The button text should switch between “Active” and “Inactive”. However, you should not see any new log messages from the child component in the console. This confirms that the memoized component is not re-rendering unnecessarily and the optimization is working as expected.

Why It Works This Way?

When you click the toggle button: 

  1. Parent Component Re-renders: The active state is updated, causing the App component to re-render. 
  2. useMemo Returns Cached Data: Since the search value has not changed, useMemo returns the previously cached product list instead of running the filter logic again. 
  3. useCallback Preserves Function Reference: The handleIncrement function reference remains unchanged because its dependencies have not changed. 
  4. React.memo Skips Re-rendering: The Child component receives the same props, so React.memo prevents it from re-rendering. As a result, no new console.log messages appear in the console. 
  5. React.PureComponent Skips Re-rendering: The PureChild component performs a shallow comparison of its props and state. Since its props remain unchanged, React.PureComponent skips the render process and reuses the existing output. 
  6. Improved Performance: By avoiding unnecessary calculations and component re-renders, the application performs more efficiently.

Final Thoughts

React memoization is a useful way to make applications faster when components repeatedly perform unnecessary work. Techniques such as memo, useMemo, useCallback, and PureComponent can help control re-renders and avoid repeating costly calculations. However, adding it without a real performance reason can make code harder to maintain and may even use more memory than necessary. The best approach is to first identify where performance is actually affected, then apply memoization where it provides a clear benefit. 

FAQs

What is the Purpose of Memoization in React?

Memoization in React optimizes performance by caching the results of expensive calculations or component renders. It prevents unnecessary re-renders and recalculations when a component’s props or state haven’t changed. React handles this through hooks like useMemo() and useCallback(), or the React.memo function.

What is the Concept of Memoization in React?

Memoization in React is an optimization technique that speeds up your app by caching the results of expensive calculations or component renders.

When to Use Memoization in React?

In React, you should use memoization only when you have identified a specific performance bottleneck or need to maintain referential stability. Memoization carries a memory and overhead cost due to storing cached values and executing dependency checks.

When Should You Not Use Memoization?

Avoid memoization when functions execute fast or consume too much memory to store results. Caching is just a waste of memory if inputs rarely repeat. It is also harmful for impure functions that depend on changing external data, like system time or live API calls, as you will get incorrect outputs.

profile-image
Parind Shah

Parind Shah is responsible for frontend innovations at TatvaSoft. He brings profound domain experience and a strategic mindset to deliver exceptional user experience. He is always looking to gain and expand his skill set.

Comments

Leave a message...

Ready to Build Your Custom Application Solution?

Tatvasoft is a reputed CMMI level 3 software and mobile app development company. When it comes to software development companies, Tatvasoft strives to be the best.

Request a Proposal Arrow Icon
United States Office
United States +1 503 832 4034
17304 Preston Road, Suite 800, Dallas, Texas, 75252 +1 503 832 4034
United Kingdom Office
United Kingdom +44 742 409 8452
307, Euston Road,
London NW1 3AD,
United Kingdom
+44 742 409 8452
Australia Office
Australia +61 3 9581 2659
Level 19/180,
Lonsdale St, Melbourne
VIC 3000
+61 3 9581 2659
Canada Office
Canada +1 416 567 7664
4711 Yonge Street,
10th Floor, Toronto, Ontario, M2N 6K8
+1 416 567 7664
Japan Office
Japan
902 Pearl Building,
Miyamae-cho 8-15, Kawasaki-ku,
Kawasaki-shi, Kanagawa,
210-0012
Saudi Office
Saudi Arabia +966 552 325 560
6th Floor,
Al Budoor Tower Prince Mohammed Bin Fahad Road,
Dammam 34251
+966 552 325 560
India Office
India +91 960 142 1472
TatvaSoft House,
Rajpath Club Road, Ahmedabad, Gujarat,
380054
1401-1409, RK Empire,
150 Feet Ring Road,
Rajkot, Gujarat,
360004
+91 960 142 1472