DEV Community

Cover image for Overcoming single-threaded limitations in React Native
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Overcoming single-threaded limitations in React Native

Written by Raphael Ugwu✏️

React Native was designed to bridge gaps between web and mobile frameworks in software development. Unfortunately, developers face certain hurdles related to performance when working with React Native.

Every framework has its limitations, what matters is how we work around them and build fully functional applications. In this blog post, we’ll take a look at certain features that limit React Native’s performance and how we can reduce their effects to build robust products with this framework.

React Native’s architecture – how it all works

To understand the problem, let’s first take a look at how React Native’s architecture works. There are three threads that mainly run a React Native app:

  • The UI thread — this is the native thread used to run Swift/Objective C in iOS devices and Java/Kotlin in Android devices, an application’s UI is manipulated solely on this thread. Here the application’s Views are rendered and users of the app can interact with the OS. Most of the heavy lifting in this thread is performed by React Native
  • The JavaScript thread — this is the thread that executes JavaScript separately via a JavaScript engine. An application’s logic – including which Views to display and in what manner they are displayed — is usually configured here
  • The bridge — React Native’s bridge enables communication between the UI and JS thread

Now, the UI and JavaScript threads are individually fast but where performance issues occur is during the communication between both of them via the bridge. Say you are passing huge files between both threads, this could slow down performance. It’s important to keep passes between both sides of the bridge to a bare minimum to avoid any kind of performance-related issues.

Because React has a virtual DOM, it renders JavaScript components asynchronously and in this process, reduces the amount of data that needs to be sent over the bridge. However, this doesn’t prevent a couple of performance issues from springing up from time to time, let’s highlight these issues and how we can fix them.

LogRocket Free Trial Banner

Single-threaded limitations

React Native is single-threaded in nature. In its rendering process, rather than have multiple processes occur at the same time (multithreading), other components have to wait when one component is being rendered.

This proves to be a huge challenge for apps that may want to implement multiple features simultaneously such as a streaming service that needs a live chat feature alongside a live stream feed. High-end devices with more RAM and processing power may get along fine but cheaper devices wouldn’t be able to run apps like Mixer as shown below:

Screenshot taken from Mixer, a live stream gaming app
Screenshot taken from Mixer, a live stream gaming app

The fix to single-threaded limitations in an app is for engineers to build maintainable extensions that can handle multithreading in a React Native app. An extension lets you provide an app with custom functionality that it would otherwise not have. Extensions can be built using either Java, Swift, or Objective-C. A great example of an extension that fixes the single-threaded issue is one that creates a bridge between React Native and Native components.

When building extensions, it’s important to perform tests on a real device and not just a simulator as real apps are likely to exceed the memory limits of an app thus resulting in memory leaks (which we’ll discuss later in this article). Apple’s Xcode Instruments remains a handy tool for checking memory usage in apps.

Slow navigator transitions

Another scenario where single-threaded limitations can be seen in a React Native app is during animation transitions. The JavaScript thread is responsible for controlling navigator animations in a React Native app.

react native animations

When React Native is trying to render a new screen while an animation is running on the JavaScript thread, it results in broken animations. React Native’s InteractionManager API is a great way to improve slow navigation transitions.

Let’s say you have an app that does location tracking where users can locate each other by listing location changes frequently. Location changes are listed by initiating a function that searches for a location at a certain time interval.

onChangeTab(event) {
    if (event === 1) {
        intervalId = BackgroundTimer.setInterval(() => {
            this.props.actions.getAllLocationAction();
        }, 5000);
    } else {
        BackgroundTimer.clearInterval(intervalId);
    }
    this.setState({
        activeTab: event
    });
}
Enter fullscreen mode Exit fullscreen mode

This repeated action is bound to create some lag in movement between components. To invoke onChangeTab repeatedly without slowing down the rendering of the UI, we’ll use the runAfter Interactions() method in the InteractionManager API which lets us delay all our operations until all animations are complete:

import { InteractionManager } from 'react-native';

onChangeTab(event) {
    if (event === 1) {
        InteractionManager.runAfterInteractions(() => {
            this.props.dispatchTeamFetchStart();
        });
    }
    this.setState({
        activeTab: event
    });
}
Enter fullscreen mode Exit fullscreen mode

Memory leaks

React Native apps, both on Android and iOS platforms, struggle to face the issue of memory leaks. Because React Native apps are powered by JavaScript, their memory is managed by the Garbage Collector – a background process that constantly reviews objects and modules and deallocates memory from the ones that are not referenced directly or indirectly from root objects.

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.

With React Native’s architecture, each module is attached to a root object. Core React Native modules declare variables that are kept in the main scope. These variables may retain other objects and prevent them from being garbage collected.

A common practice in React Native apps that can lead to memory leaks is improper handling of closures. Closures are functions that capture variables from parent scopes. Check out the code sample below:

var thisList = null;
var replaceList = function () {
  var originalList = thisList;
  var idle = function () {
    if (originalList)
      console.log("nice");
  };
  thisList = {
    thisArray: new Array(2000000).join('*'),
    thisMethod: function () {
      console.log(thisMessage);
    }
  };
};
setInterval(replaceList, 1000);
Enter fullscreen mode Exit fullscreen mode

In the above code sample, for every time replaceList is called, thisList gets an object which contains an array (thisArray) and a method thisMessage. Simultaneously, the variable idle holds a closure that refers to originalList which refers to its parent function replaceList. The scope created for the closure thisMethod is shared by the variable idle, which — even though it is never used — its indirect reference to originalList makes it stay active and unable to be collected by the Garbage Collector.

Thus when replaceList is called repeatedly, a steady increase in memory usage can be observed which doesn’t get smaller when the Garbage Collector runs. What this means is that each of the linked lists of closures created carries an indirect reference to thisArray thus resulting in a costly memory leak.

Fortunately, fixing memory leaks that occur as a result of closures is straightforward. Just add originalList = null to the end of replaceList. So even though the name originalList is still in the lexical environment of thisMethod, there won’t be a link to the parent value thisList:

var thisList = null;
var replaceList = function () {
  var originalList = thisList;
  // Define a closure that references originalList but doesn't ever
  // actually get called. But because this closure exists,
  // originalList will be in the lexical environment for all
  // closures defined in replaceList, instead of being optimized
  // out of it. If this function is removed, there is no leak.
  var idle = function () {
    if (originalList)
      console.log("nice");
  };
  thisList = {
    thisArray: new Array(2000000).join('*'),
    thisMethod: function () {}
  };
  // If you add `originalList = null` here, there is no leak.
  originalList = null
};
setInterval(replaceList, 1000);
Enter fullscreen mode Exit fullscreen mode

In the code sample above, while originalList is accessible to thisList, it doesn’t use it. But because originalList is a part of the lexical environment, thisMethod will hold a reference to originalList . Thus even if we are replacing thisList with something that has no effective way to reference the old value of thisList, the old value won’t get cleaned up by the garbage collector. If you have a large object that is used by some closures but not by any closures that you need to keep using, just make sure that the local variable no longer points to it once you’re done with it.

Conclusion

React Native is an awesome framework that fuses web and mobile development. Applications can be written for Android and iOS devices using just one language – JavaScript. Though it may have shortcomings with impacting on the overall performance of an application, most of these shortcomings can be avoided or improved upon to create an overall better user experience for mobile apps.


Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Overcoming single-threaded limitations in React Native appeared first on LogRocket Blog.

Top comments (0)