Memory leak in React JS application

Memory leak in React JS application

Memory leak or memory gain in a react application is risky, sometimes for this may be the browser tab or maybe the whole browser could be crushed!!

Recently I faced a problem with that, the scenario is like this, we have a react application that is serving a lot of charts with multiple pages, On a single page there were mostly 5-7 charts, for the charts we use Apache eCharts.

The problem

See the example chart below, we have 6 charts like this on an individual page.

Each of the charts has an individual chart option, where the user can change the x-axis length y-axis scale & etc.

This application will load once and stay idle until refresh, for the chart data there was an API call every one minute, and the chart data will update automatically.

So there were six charts and each chart had a minimum of one or two API calls each minute and the page stayed idle for a minimum of 7-30 days.

The component tree looks like this.

SitePage
├── ReactGridLayout
│   ├── Components loop
│   │   ├── SiteChartContainer
│   │   ├── API call
│   │   │   ├── DataChart
│   │   │   └── SingleValue
│   │   │   └── ChartSettings

There was another child component in the DataChart, SingleValue, ChartOptions

So every time a new API is called the Single Component is rerendered, and we tested the memory it increased approximately by 1MB every 10 minutes.

And after 1 or 2 days or maybe 7+ days, the memory increases by ~1GB and then the tab is crushed, sometimes the full browser.

The Solutions

At first, when the browser was crushed we look everything to determine the issue, why this shit was happening?

We investigated again and again, and finally, we found that the application memory gradually increased a huge amount.

Identifying the issue

In our react application there were a lot of event listeners like, click & mouse event listeners also we had some timer functions which are not clear when a component did unmount, for this, the consumed memory taken by the listener, timer and hooks are not released, also the JS garbage collector did not clear those garbages.

Also, our chart has some issues like formatting the chart tooltip with a closure function where our whole series is dependent on some of the calculations.

After the investigation, we assumed these were going wrong with many files, and after finding the issue, it was easy to take action, so we checked every component related to this page and did the necessary updates with those files.

Optimizing the component hierarchy

As you see in our previous components hierarchy, we decided to refactor this, mainly the main component SiteChartContainer.

Previously we used the react context-API to pass the data to the child component, but we found that the context-API is not a good solution for this, because it re-rendering the whole component tree when the context value is changed, so we decided to use the zustand for this, and we also use the react memo for the child component.

01 - The new component hierarchy

SitePage
├── ReactGridLayout
│   ├── Components loop
│   │   ├── SiteChartContainer
│   │   ├── ├── DataFetcher
│   │   │   ├── DataChartDom
│   │   │   └── DataChartOptions
│   │   │   └── SingleValue
│   │   │   └── ChartSettings

We separate the SiteChartContainer components into Stack/Siblings components from Tree/Nested components. And pass the data to the child component with the Zustand store. So now only the respective component will re-render when the data is changed.

Responsible components

  • SiteChartContainer - Contains all the initialization and sibling components

  • DataFetcher - Fetch the data from the API and pass the data to the zustand store

  • DataChartDom - Render the eChart with react-dom reference

  • DataChartOptions - Set the initial eChart options and update the options when the data is changed

  • SingleValue - Render the latest value of the chart from the zustand store

  • ChartSettings - Render the chart settings and update the chart options when the settings are changed

02 - Add clean-up functionality - Timers & Listener

We also refactored the listener and timer, we used the useEffect hook to add the listener and timer and clear the listener and timer when the component unmounts.

JS provides built-in functions to execute code asynchronously after a specific duration (setTimeout) or at regular intervals (setInterval) and addEventListener for the DOM manipulation through clicks or other events.
While they're powerful, they can inadvertently cause memory gain if not clear the timers or remove the listeners.

A simple example of the listener and its clear functionality

useEffect(() => {
  const listener = () => {
    // do something
  }
  window.addEventListener('click', listener)
  return () => {
    window.removeEventListener('click', listener)
  }
}, [])

if you do not remove these listeners while unmounting the component, the listener will trigger every time and add a new listener to the heap memory in every rerender of the component.

Another example of clearing up the timer functions

useEffect(() => {
  const timer = setInterval(() => {
    // do something
  }, 1000)
  return () => {
    clearInterval(timer)
  }
}, [])

Remember whenever you add an EventListener or Timers function do not forget to remove or clear those.

03 - Refactor the chart tooltip

This part is pretty tricky, we found that the chart tooltip is not releasing the memory and this is an internal issue of eCharts, so we pass the necessary data to the series value for the tooltip when the chart data is calculated the series in DataChartOptions component, and we also use the format function to format the tooltip.

type ChartSeriesData = [
    number, // timestamp
    number, // value
    string, // unit
    string|object, // necessary data for tooltip
]

So the the tooltip is not dependent anymore on the series data, and the memory is released.

The Result

After the optimization, we found that the memory is not increasing too much, and the application has been running smoothly for more than 20+ days.

We also refactored the MUI material theme switcher functionality, cause when we switched a theme from light-to-dark or dark-to-light previously it was re-rendering the whole page.

JS Garbage collector and manually clean-up

Some high-level languages, such as JavaScript, utilize a form of automatic memory management known as garbage collection (GC). The purpose of a garbage collector is to monitor memory allocation and determine when a block of allocated memory is no longer needed and reclaim it. This automatic process is an approximation since the general problem of determining whether or not a specific piece of memory is still needed is undecidable.

As stated above, the general problem of automatically finding whether some memory "is not needed anymore" is undecidable. As a consequence, garbage collectors implement a restriction of a solution to the general problem. This section will explain the concepts that are necessary for understanding the main garbage collection algorithms and their respective limitations.

To be safe here is a checklist for you.

  1. Timers and Callbacks

  2. Closures

  3. Event Listeners

  4. Detached DOM Elements

  5. API calls like React Query, Axios

Tools for testing memory leak

  • Browser DevTools - Memory Tab

  • Memlab - by Meta

Conclusion

We learn a lot from this issue, and we also learn that the react context-API is not a good solution for the data passing. In this issue, we also learn how to use the Zustand store more deeply.

The solution I writing here maybe it seem easy to think, but in our case, the solutions come after a lot of R&D, also these parts are not only the solutions, but these 3 solutions are the main parts.

This is not a one-man-army effort, all things were done by the team, the Avengers!! 😎