Handling Memory Leaks in React for Optimal Performance
Introduction
Dealing with memory leaks in React? We've got your back. Well, React makes an excellent choice for building dynamic web solutions, but when there is poor memory management, it can slow down your app's performance, slow page loads, and even cause the app to crash. We are here with a comprehensive guide to help you understand how to detect and handle memory leaks in React to enjoy optimal performance.
What is Memory Leak?
Memory leaks remain a common issue in the React app, and they commonly occur when an app continuously uses more memory but fails to release unused memory back to the system. In the context of ReactJS, memory leaks can happen when components hold onto references that are no longer needed, preventing the garbage collector from freeing up memory. This can happen due to improper handling of subscriptions, timers, or event listeners in the component lifecycle.
Such leaks often go unnoticed during development but ultimately result in performance issues as the app expands.
Common Causes of Memory Leaks in React
Here are the common causes of memory leaks that you might find in React.
Event Listeners: Adding event listeners in useEffect without removing them in a cleanup function.
Timers/Intervals: Setting up setTimeout or setInterval without clearing them keeps them running in the background.
Subscriptions: Subscribing to external data sources like websockets, Firebase, or custom events without unsubscribing.
Global Reference: Keeping references to components or DOM nodes in the global state that are not cleaned up.
Improper State Management: Attempting to update state after a component unmounts, often in async operations like API calls.
Code Example: Memory Leak in SPA
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.
Example 1: SPA that Toggles the Content Between Home Page and About Page
Note: An event listener is attached to the component. Therefore, each time the component is mounted into the DOM, the useEffect hook is called, creating a fresh copy of the event listener.
However, when toggling between <Home/> and <About />, only the HTML content changes, but the attached event listener continues running after it is unmounted.
This happens because the memory subscription for the event listener was not removed during the unmounting of <About />. When we first navigate to /about, memory is allocated for the event listener, but without cleanup, it persists, continuing its work even after <About /> is unmounted.
When we visit /about again, both the previous event listener and the newly created one execute. This cycle repeats with each toggle between the <Home/> and <About /> components, accumulating listeners.
This may not significantly impact a small app like this, but it can severely degrade performance in large-scale React applications if not addressed.
To resolve this, we must cancel the memory subscription when the component unmounts. This can be achieved using the cleanup function in the useEffect hook.
The refactored component is shown below:
About.js
// About.js
import { useEffect } from "react";
const About = () => {
useEffect(() => {
const handleClick = () => {
console.log("Window clicked!");
};
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick);
}, []);
return <>About page;
};
export default About;
With this change, we unsubscribe the memory allocated to the event listener each time it unmounts, preventing memory leaks and improving overall performance.
App.js
// App.js
import { BrowserRouter, Route, Routes, Link } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import "./styles.css";
export default function App() {
return (
About Page
Home Page
} />
} />
);
}
Home.js
// Home.js
const Home = () => {
return <>Home page;
};
export default Home;
Note: There is an event listener attached to the <About /> component. Therefore, each time the <About /> component is mounted into the DOM, the useEffect hook will be called, and a fresh copy of the event listener will be created.
Example 2: Handling Web Requests with Slow Internet Connection
Let’s say in one of your React components, you are making an HTTP request to fetch data from a server, process it, and set it into a state variable for UI generation.
However, if the user’s internet connection is slow and they navigate to another page before the response arrives, the browser still expects a response from the pending request, even though the page has changed.
Consider the example below:
App.js
// App.js
import { BrowserRouter, Link, Routes, Route } from "react-router-dom";
import About from "./About";
import Home from "./Home";
export default function App() {
return (
<>
About
Home
} />
} />
>
);
}
Home.js
// Home.js
const Home = () => {
return <>Home page;
};
export default Home;
About.js (Original)
// About.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;
In the above component, we fetch data from a server, perform extensive calculations, and set it into the data state variable. When the user navigates from the Home page to the About page, the API call is made as soon as <About /> mounts into the DOM.
If the user has a slow internet connection, the server response may be delayed. If they navigate back to the Home page before the response arrives, the pending API request continues running in the background. Once the data is received, the calculations are performed, even though the component, which needs the data, is unmounted.
While the state update (setData) is prevented by React’s garbage collection, the API request and calculations still consume server and client resources unnecessarily, increasing maintenance costs.
Fortunately, JavaScript provides the AbortController API to cancel HTTP requests when needed. The AbortController represents a controller object that allows aborting one or more web requests.
Here is the refactored component:
About.js (Refactored)
// About.js
import { useEffect, useState } from "react";
import axios from "axios";
const About = () => {
const [data, setData] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/users",
{ signal: abortController.signal }
);
// some extensive calculations on the received data
setData(data);
} catch (err) {
if (err.name === "AbortError") {
console.log("Request aborted");
} else {
console.log(err);
}
}
};
fetchData();
return () => {
abortController.abort();
};
}, []);
return (
<>
{/* handle data mapping and UI generation */}
About Page
);
};
export default About;
We added a cleanup function in useEffect that uses abortController.abort() to cancel the HTTP request and associated calculations when <About /> unmounts.
Note: Aborting the request triggers the catch block with an AbortError. Handle the catch block appropriately to distinguish between abort errors and other errors.
By using the AbortController API, we optimize server resources and prevent unnecessary computations, improving performance in large-scale React applications.
How to Detect Memory Leak?
Let's look for some steps and techniques to detect memory leaks in a React app.
Utilize Chrome DevTools
Open DevTools > go to Memory > take Head Snapshot at intervals.
Compare the snapshots to identify retained objects that should have been garbage collected but still exist.
React Developer Tools
Install React Developer Tools
- Use Profiler tabs to record renders during interactions.
- Look for components that re-render frequently.
Inspect components that don't unmount properly.
Look for Warnings
React displays a warning like "Can't perform a React state update on an unmounted component" to indicate memory leaks from async tasks or effects.
Strict Mode
Enable <React.StrictMode> in development to catch lifecycle issues, such as deprecated APIs or unsafe side effects.
Third-party Tools
- Install the Why-did-you-render library to detect unnecessary re-renders caused by improper memoization or state updates.
- Run Lighthouse in Chrome DevTools to identify potential memory issues.
Best Practices to Prevent Memory Leaks
Let’s check for some best practices to prevent memory leaks in React.
Avoid Unnecessary State and Context: Ensure that state and context only store essential data and are reset properly when no longer required.
Cancel API Requests: Utilize an abort controller to cancel ongoing API requests when the component unmounts.
Clear Timers: Use the cleanup function in useEffect to clear any timers set within the component.
Event Listeners: When a component unmounts, use the useEffect hook with a cleanup function to remove event listeners.
Rendering Optimization: Use techniques like React.memo to control unnecessary re-renders.
Debounce/Throttle Event Handlers: Use libraries like Lodash to restrict rapid event triggers that might queue extreme updates.
Test Component Unmounting: Run tests to simulate mounting/unmounting components and verify cleanup.
Avoid Storing Big Data in State: Store minimal data in component state and offload heavy computations to utilities or backend APIs.
Final Thoughts
In this blog, we discussed what a memory leak is, how to detect it, and best practices to prevent memory leaks. We also checked the example of memory leak in SPA with a particular solution and how to unsubscribe unwanted memory using cleanup functions.
Need experts to tackle challenges in your React apps? Hire React native developers with us. Our developers have the expertise and experience to help you resolve memory leaks and ensure optimal performance.