DEV Community

Cover image for Handling Memory Leaks in React for Optimal Performance
Dave Rushabh
Dave Rushabh

Posted on • Originally published at lucentinnovation.com

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 js 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.

What are 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 fetch the entire JavaScript bundle from the server on initial request and then handle the rendering of each page 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> 
  ); 
} 
Enter fullscreen mode Exit fullscreen mode
const Home = () => { 
  return <>Home page</>; 
}; 

export default Home; 
Enter fullscreen mode Exit fullscreen mode
import { useEffect } from "react"; 

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

export default About; 
Enter fullscreen mode Exit fullscreen mode

Note
There is an Event Listener attached to the element. Therefore, each time the 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 and only the HTML content will be changed but the attached Event Listener will be running even after is unmounted.
  • This happens because the Memory subscription to run the Event Listener was supposed to be removed while unmounting the 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 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 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; 
Enter fullscreen mode Exit fullscreen mode

With this small change, we are able to unsubscribe the memory allocated to the Event Listener every time the 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> 
    </> 
  ); 
} 
Enter fullscreen mode Exit fullscreen mode
const Home = () => { 
  return <>Home page</>; 
}; 

export default Home; 
Enter fullscreen mode Exit fullscreen mode
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; 
Enter fullscreen mode Exit fullscreen mode

In the above 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 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 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; 
Enter fullscreen mode Exit fullscreen mode

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 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:

  1. 1. What is Memory Leaks in React
  2. 2. Why SPAs are prone to memory leaks?
  3. How to handle memory leaks by unsubscribing unwanted memory using Cleanup functions and AbortController APIs

Thanks!!

Top comments (0)