At Software Mansion we are often asked by our clients to review their React Native apps for possible performance improvements. One of the most common reasons of performance issues are memory leaks. In this article, we gathered the typical approaches to debug and solve memory problems in a React Native app. If your app happens to suffer from memory issues, you will learn how to tell if your app is leaking memory, and if so, how to pinpoint and fix the source of the leak.

Memory leaks in JS

In JavaScript memory is managed automatically by Garbage Collector (GC). In short, Garbage Collector is a background process that periodically traverses the graph of allocated objects and their references. If it happens to encounter a part of the graph that is not being referenced directly or indirectly from root objects (e.g., variable on the stack or a global object like window or navigator ) that whole part can be deallocated from the memory.

It is a common misconception that languages which rely on Garbage Collection (GC) for memory management (like JavaScript) are resilient to memory leaks.

In React Native world each JS module scope is attached to a root object. Many modules, including React Native core ones, declare variables that are kept in the main scope (e.g., when you define an object outside of a class or function in your JS module). Such variables may retain other objects and hence prevent them from being garbage collected.

Here is a list of most common mistakes in React Native apps that can lead to memory leaks:

1. Unreleased timers/listeners added in componentDidMount

This is a no. 1 reason for memory leaks in React apps in general. Let’s look at the following component that listens to keyboard showing and hiding events:

Here, we register listeners to keyboardDidShow and keyboardDidHide events to keep current keyboard status as a state of the component. The listeners are registered when the component gets mounted. Since we never remove these listeners, they will still receive keyboard events even after component gets unmounted. At the same time, Keyboard module has to keep the list of active listeners in global scope — in our case, it will retain the arrow functions we pass to addListener method. In turn, these arrow functions retain this – i.e., the reference to the Composer component, which in turn references its properties via this.props , its children via this.props.children , its children’s children, etc. This simple mistake can lead to very large areas of memory left retained by accident.

The above should be resolved by properly removing listeners in componentWillUnmount :

Always remember:

If your component registers listeners or uses setTimeout , setInterval or similar method to run a callback function, you have to make sure that these listeners and callbacks are properly cleaned up when component unmounts.

One of the strategies that will help you avoid problems like this is to place registering and unregistering logic in a wrapper component that provides the data from events to its children via props. This way we will limit the number of places in our codebase where we need to register and unregister listeners. As an example we recommend you to check out this article by Eric Kim. It describes a simple HOC-based approach that wraps Keyboard callbacks.

2. Closure scope leaks

Closure scope leaks are much more difficult to avoid — but not necessarily harder to track down.

A closure scope is an object containing all the variables outside of closure that are being referenced from within that closure.

Let’s imagine we want to build a countdown timer component. It takes initial time as props and keeps the current time that is left in its state . To update the timer, we will use setInterval in componentDidMount :

In componentDidMount we create an instance of arrow function (closure) that will update state. In order for that arrow function to work, it needs to refer to some variables from the scope where the function is defined, namely hours , mins , sec and this (as it uses this.setState or this._interval ). In Javascript VM an arrow function instance is represented as an object that retains references to all the variables it “captures.” Note that countdown variable is also defined in the same scope as the arrow function, but since it is not “captured” by the arrow, it won’t be retained by the function.

Now imagine that we would like to handle prop updates — in this case, to reset the countdown to values passed as the new props. We will use componentDidUpdate lifecycle method for that. Now, if any of the countdown related props changes (that is countdown , hours , mins or sec ), we will restart interval callback:

In componentDidUpdate we use the same code to create an interval callback, so we expect it to retain exactly the same variables as before. However, this isn’t the case: our arrow function will retain prevProps in addition to the other referred variables.

The reason for prevProps being retained is that in a given parent scope Javascript VM will only create a single scope object that is then shared between all closures created in that parent scope. In this case, the scope object is shared by all closures defined in componentDidUpdate , so it will retain all variables used in at least one closure there. Unfortunately, there is a closure in line 3 that captures prevProps . Even though that closure is not being used later on and is definitely not being retained after it is used to calculate clockPropsHasChanged, it still makes all the other closures retain prevProps .

Note that capturing prevProps in componentDidUpdate may in some cases have serious consequences, especially when our component has children. In such case, we will end up capturing references to unmounted children via prevProps.children .

As our experience shows, lifecycle methods that accept previous state or props (i.e., componentDidUpdate or getDerivedStateFromProps ) are the most common source of memory leaks via closure scope in React apps.

There are a handful of ways how that kind of scope leak can be eliminated:

We can assign prevProps = null right after we are done with calculating clockPropsHasChanged . We can extract method responsible for comparing props and define it at the component level, then pass prevProps to it and call it as follows: clockPropsHasChanged = this.compareClockProps(prevProps) . This way the closures from our example are going to be created in separate scopes. Similarly, we can extract a method that creates setInterval callback, e.g.: this.createUpdater(hours, mins, sec)

Does my app leak memory?

Usually, it is quite difficult to tell if the app is leaking — especially that sometimes the leaks are so small that it is virtually impossible to notice. The best approach is to analyze a workflow within your app that you would expect to be memory neutral, i.e., a sequence of steps that should not lead to any new objects being retained. For example, navigating to a new screen and going back to the previous, or adding and removing items from a list are both scenarios that in most cases should not increase memory usage. If such a workflow leads to leaking, you should notice your app’s memory consumption to grow after you repeat it several times.

The easiest way to observe that is by using Instruments on iOS or Android Studio Profiler tab for Android. Both of those tools show the total memory usage of the app — this includes both memory allocated by JS, as well as the memory allocated by native modules and views. Let’s take a look how to use them:

I. Monitoring memory usage on iOS

After launching the app from Xcode, go to “Debug navigator” (step 1) and select memory section (step 2):

Opening memory monitor in Xcode

Use your app for a while and see how memory usage behaves:

Memory usage grows when screen is opened but does not get back to normal when we close the screen

If the memory usage increases after a sequence of memory neutral actions, the app is likely leaking memory.

2. Monitoring memory usage in Android Studio

When testing your app on Android, you can use Android Studio Profiler tools. First, open your project with Android studio. When you have your device or emulator connected, and your app launched, you should navigate to the “Profiler” tab on the very bottom and select “memory” section:

Monitoring memory usage on Android

Inspecting JS memory with Safari

It’s occurred to me recently that not many people know that React Native apps can be inspected using Safari Web Inspector tool. It has pretty decent support for memory snapshots: it will let you compare older and newer snapshot to filter out objects created and retained recently. A big advantage of using Safari Web Inspector over React Native’s Remote Debugging is that we will interact with the same type of Javascript virtual machine as our app is running in production (i.e., JavaScriptCore, instead of the V8 engine used when debugging using Chrome).

Following steps will connect Safari Web Inspector to your React Native app running on iOS:

1. Enable “Develop menu” in Safari

Launch Safari on your Mac and navigate to the “Advanced” section in settings. Make sure that you have the following field checked (“Show Develop menu in menu bar”):

Enable develop menu in Safari settings

2. Connect Web Inspector to JSC context running on simulator or Device

When your React Native app is running on a connected device or simulator, you should be able to see Javascript context (JSContext) under “Develop” menu in the second section: