By Rushabh Daveauthor-img
July 17, 2023|11 Minute read|
Play
/ / Handling Memory Leaks in React for Optimal Performance

Introduction

Do you remember when Uncle Ben said, "With great power comes great responsibility"? It turns out that the same holds true for React development. As React gains more and more popularity for frontend development, there are some bottlenecks that may eventually lead to less performant apps. One such reason could be Memory Leaks in React.

Key Takeaway

Explore the article to understand the concept of Memory Leaks in React, why React apps, especially Single Page Applications (SPAs), are susceptible to such leaks, and practical solutions using cleanup functions and AbortController APIs to optimize server resources and prevent Memory Leaks in large-scale applications.

What is Memory Leaks?

Memory leaks occur when a computer program, in our case a React application, unintentionally holds onto memory resources that are no longer needed. These resources can include variables, objects, or event listeners that should have been released and freed up for other operations. Over time, these accumulated memory leaks can lead to reduced performance, slower response times, and even crashes.

Why React Apps are prone to Memory Leaks?

React by far is widely used for creating Single Page Applications (SPAs), these SPAs fetches the entire JavaScript bundle from the server on initial request and then handles the rendering of each pages on the client side in the web browsers.

Note

React apps with SPA configuration do not entirely refresh when the URL path is changed, it just replaces the HTML content by updating the DOM tree through its Reconciliation process.

So, we have to be mindful while subscribing to memory in our React components because React will eventually change the HTML content according to any given page but the associated memory subscriptions (which could be a DOM Event listener, a WebSocket subscription, or even a request to an API ) may still be running in the background even after the page is changed !!

To better understand, consider the following examples:

1.) SPA that toggles the content between Home page and About page

import { Route, Routes, Link } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import "./styles.css";

export default function App() {
return (
<div className="App">
<Link to="/about">About Page</Link>
<br />
<Link to="/">Home Page</Link>
<br />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
);
}

App.js

const Home = () => {
return <>Home page</>;
};

export default Home;

Home.js

import { useEffect } from "react";

const About = () => {
useEffect(() => {
setInterval(() => {
console.log("Hello World !");
}, 200);
}, []);
return <>About page</>;
};

export default About;

About.js
Note

There is an Event Listener attached to the <About /> element. Therefore, each time the <About /> element is mounted into the DOM, the useEffect will be called and a fresh copy of the Event Listener will be created.

  • But, if you closely observe, when toggling between <Home/> and <About /> only the HTML content will be changed but the attached Event Listener will be running even after <About /> is unmounted.
  • This happens because the Memory subscription to run the Event Listener was supposed to be removed while unmounting the <About /> but we forgot to do that.
  • Which means, there is some memory allocated to run an Event Listener when we first time navigate to “/about”, but that memory was not cleaned. so the event listener will keep on doing its work even when the <About /> is unmounted.

🔴 When we visit the “/about” next time the previous event listener and the newly created one both will start their execution. This cycle will keep on repeating as many times you toggle between these two components.

⚠️ This might not be significant for a small app like above but can seriously damage the overall performance if not handled in large scale React apps.

💡 To Overcome this issue, all we need to do is to cancel the memory subscription during the component’s unmounting time. We can do this with the clean up function inside the useEffect.

So, the refactored <About /> component will look like below :

import { useEffect } from "react";

const About = () => {
useEffect(() => {
const interval = setInterval(() => {
console.log("Hello World !");
}, 200);

return () => {
clearInterval(interval);
};
}, []);
return <>About page</>;
};

export default About;

About.js

With this small change, we are able to unsubscribe the memory allocated to the Event Listener every time the <About /> unmounts, which tackles the Memory Leaks and Improves the Overall performance.

2.) Handling with web requests with slow internet connection

Let’s say in one of your React components you are making an HTTP request that fetches the data from the server and later on after some processing on it, we want to set it into the state variable for UI generation.

But there is a catch, what if the user’s internet connection is slow and decides to move to another page ! in that case the web requests is already made so browser will expect some response even though the page is changed by user.

consider the below example:

import { Link, Routes, Route } from "react-router-dom";
import About from "./About";
import Home from "./Home";

export default function App() {
  return (
    <>
      <div className="App">
        <Link to="/about">About</Link>
        <br />
        <Link to="/">Home</Link>
        <br />
      </div>

      <Routes>
        <Route path="/about" element={<About />} />
        <Route path="/" element={<Home />} />
      </Routes>
    </>
  );
}

 App.js

const Home = () => {
  return <>Home page</>;
};

export default Home;

Home.js

import { useEffect, useState } from "react";
import axios from "axios";

const About = () => {
const [data, setData] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
// some extensive calculations on the received data
setData(data);
} catch (err) {
console.log(err);
}
};
fetchData();
}, []);

return (
<>
{/* handle data mapping and UI generation */}
About Page
</>
);
};

export default About;

About.js
  • In the above <About /> we are fetching the data from a server and then doing some extensive calculation and then setting it into the state variable.
  • Let’s say user navigates from Homepage to About page. As soon as the <About /> gets mounted into the DOM the API call will be made.
  • The user is having slow internet connection, due to which the server response is delayed and the user decides to leave the page and move back to Homepage.
  • When user moves back to the Homepage, the pending API request will still be running in the background and once the API data is received the extensive calculation will also be calculated, even though the component which needs that data is unmounted !!
  • However the setting of that calculated value into the state variable will not take place as it is going to be garbage collected, but still why do we need to make that API request and perform the calculations when the component itself is unmounted ?

If this is not take care of, it has a potential to unnecessarily occupy the server resources which indeed affect the maintenance cost of the servers.

Luckily, JavaScript provides a way to cancel the HTTP request whenever we want. via AbortControllers APIs. It represents a controller object that allows you to abort one or more Web requests as and when needed.

Look at the refactored <About /> below:

import { useEffect, useState } from "react"; 
import axios from "axios"; 
 
const About = () => { 
  const [data, setData] = useState(null); 
 
  useEffect(() => { 
    const abortController = new AbortController(); 
    const fetchData = async () => { 
      let signal = abortController.signal; 
      try { 
        const { data } = await axios.get( 
          "<https://jsonplaceholder.typicode.com/users>", 
          { 
            signal: signal 
          } 
        ); 
        // some extensive calculations on the received data 
        setData(data); 
      } catch (err) { 
        console.log(err); 
      } 
    }; 
    fetchData(); 
 
    return () => { 
      abortController.abort();  
    }; 
  }, []); 
 
  return ( 
    <> 
      {/* handle data mapping and UI generation */} 
      About Page 
    </> 
  ); 
}; 
 
export default About; 

  About.js

We added a cleanup function in our useEffect, which is just doing a job to abort the HTTP requests along with that extensive calculation whenever the <About /> is unmounted.

Note

That means aborting the request like above will directly get you inside the catch block when unmounting, so handle the catch block properly.

💡 Thus, by using the AbortController API, we can optimize the server resources and prevent Memory Leaks when building the large scale apps.

Conclusion

In this article you found out:

  • What is Memory Leaks in React?
  • Why SPA are prone to memory leaks?
  • How to handle memory leaks by unsubscribing unwanted memory using Cleanup functions and AbortController APIs?

Thanks!!

Q: What are Memory Leaks in React, and why are React apps, particularly SPAs, prone to these leaks?

A: Memory Leaks in React occur when the application unintentionally retains unnecessary memory resources. React SPAs, fetching the entire JavaScript bundle on the initial request, may encounter memory leaks when memory subscriptions (like DOM event listeners or API requests) persist even after a page change. This happens due to the SPA's nature, where only HTML content is replaced without a full page refresh, leading to unnoticed memory subscriptions running in the background.

Q: How can Memory Leaks be addressed in React components?

A: Memory Leaks can be mitigated by incorporating cleanup functions inside the useEffect hook, ensuring the removal of unwanted memory subscriptions during component unmounting.

Q: What is the role of the AbortController API in preventing Memory Leaks in React?

A: The AbortController API plays a crucial role in canceling HTTP requests, preventing unnecessary server resource occupation and optimizing memory usage, thus mitigating Memory Leaks in large-scale React applications.

Also, read: Why Sentry is a must tool for complex ReactJS App in 2023

Rushabh Dave

Software Engineer

One-stop solution for next-gen tech.