Improve user experience, performance, and stability of your apps

Introduction

Optimizing React Native apps with a limited development budget can be difficult but is not impossible. In such a case, you need to focus on the essentials of your app and squeeze as much as possible out of them to maintain your business continuity.

That’s why we prepared this guide.
In this and the following articles, we will show you how to optimize the performance and stability of your apps. Thanks to the practices described in the guide, you will improve the user experience and speed up the time-to-market of your apps.

The whole guide is divided into 18 articles, which will be published regularly. Over time, all these articles will be collected in one place and made available as one large ebook for download.

The guide contains best practices for optimizing the following aspects:

  • Stability
  • Performance
  • Resource usage
  • User experience
  • Maintenance costs
  • Time-to-market

All these aforementioned aspects have a particular impact on the revenue-generating effectiveness of your apps. Such elements as stability, performance, and resource usage are directly related to improving the ROI of your products because of their impact on better user experience.

With faster time-to-market, you can stay ahead of your competitors, whereas an easier and faster maintenance process will help you to reduce your spendings on that particular process.

Note: If you want to read more about how performance impacts your products ROI and why fast time-to-market is such important, read these posts:
How website performance affects conversion rates
The importance of improving your products’ time-to-market

The guide focuses on three main aspects:

First group: Improving performance by understanding React Native implementation details and knowing how to make maximum out of it.

Second group: Improving performance by using the latest React Native features or turning some of them on.

Third group: Improving the stability of the application by investing in testing and continuous deployment.

We consider these aspects to be very important in the optimization process because implementing appropriate practices in these areas can help you achieve full React Native potential.

What the guide will look like and what topics it will cover.

This article is a part of the ultimate guide to React Native optimization. The guide contains a set of some of the most important best practices that you should be doing from the very beginning.

The guide is divided into three groups:

The first group is about improving performance by understanding React Native implementation details and knowing how to make maximum out of it. Here are the topics we will discuss:

The second group is focused on improving performance by using the latest React Native features or turning some of them on. This part describes the following topics:

The third group says about improving the stability of the application by investing in testing and continuous deployment. This part says about:

  • Testing key pieces of the app
  • Working CI in place
  • Shipping fast with CD
  • A/B Tests
  • OTA updates

The structure of each article is simple:

Issue: The first part describes the main problem and what you may be doing wrong.

Solution: The second part says about how that problem may affect your business and what are the best practices to solve it.

Benefits: The third part is focused on the business benefits of our proposed solution.

OK, the informational and organizational part is already covered. Now, let’s move on to the best practices for optimizing the performance of your app.

Let’s go!

React Native takes care of the rendering. But performance is still the case.

With React Native, you create components that describe how your interface should look like. During runtime, React Native turns them into platform-specific native components. Rather than talking directly to the underlying APIs, you focus on the user experience of your application.

However, that doesn’t mean all applications done with React Native are equally fast and offer the same level of user experience.

Every declarative approach (incl. React Native) is built with imperative APIs. And you have to be careful when doing things imperatively.

When you’re building your application the imperative way, you carefully analyze every callsite to the external APIs. For example, when working in a multithreaded environment, you write your code in a thread-safe way, being aware of the context and resources that the code is looking for.

illustration that shows every declarative interface is backed by a set of imperative instructions

Despite all the differences between the declarative and imperative ways of doing things, they have a lot in common. Every declarative abstraction can be broken down into a number of imperative calls. For example, React Native uses the same APIs to render your application on iOS as native developers would use themselves.

React Native unifies performance but doesn’t make it fast out of the box!

While you don’t have to worry about the performance of underlying iOS and
Android APIs calls, how you compose the components together can make all the difference. All your components will offer the same level of performance and responsiveness.

But is the same a synonym of the best? It’s not.

That’s where our guide comes into play. Use React Native to its full potential.

As discussed before, React Native is a declarative framework and takes care of rendering the application for you. In other words, it’s not you that dictate how the application will be rendered.

Your job is to define the UI components and forget about the rest. However, that doesn’t mean that you should take the performance of your application for granted. In order to create fast and responsive applications, you have to think the React Native way. You have to understand how it interacts with the underlying platform APIs.

First Group: Improving performance by understanding React Native implementation details

In this group, we will dive deeper into the most popular performance bottlenecks and React Native implementation details that contribute to them. This will not only be a smooth introduction to some of the advanced React Native concepts but also will let you significantly improve the stability and performance of your application by performing small tweaks and changes.

The following article is focused on the first point from the whole checklist of the performance optimization tactics: UI re-renders. It’s a very important part of the React Native optimization process because it allows reducing the device’s battery usage what translates into the better user experience of your app.

Pay attention to UI re-renders

Issue: Incorrect state updates cause extraneous rendering cycles / or the device is just too slow

As discussed briefly, React Native takes care of rendering the application for you. Your job is to define all the components you need and compose the final interface out of these smaller building blocks. In that approach, you don’t control the application rendering lifecycle.

diagram that shows how React computes the diff and applies smallest amount of changes

In other words – when and how to repaint things on the screen is purely React Native’s responsibility. React looks out for the changes you have done to your components, compares them and, by design, performs only the required and smallest number of actual updates.

The rule here is simple – by default, a component can re-render if its parent is re-rendering or the props are different. This means that your component’s render method can sometimes run, even if their props didn’t change. This is an acceptable tradeoff in most scenarios, as comparing the two objects (previous and current props) would take longer.

Negative impact on the performance, UI flicker, and FPS decrease

While the above heuristic is correct most of the time, performing too many operations can cause performance problems, especially on low-end mobile devices.

As a result, you may observe your UI flickering (when the updates are being performed) or frames dropping (while there’s an animation happening and an update is coming along).

Note: You should never perform any premature optimizations. Doing so may have a counter-positive effect. Try looking into this as soon as you spot dropped frames or undesired performance within your app.

As soon as you see any of these symptoms, it is the right time to look a bit deeper into your application lifecycle and look out for extraneous operations that you would not expect to happen.

Solution: Optimize the number of state operations and remember to use pure and memoized components when needed

There are a lot of ways your application can turn into unnecessary rendering cycles and that point itself is worth a separate article. In this section, we will focus on two common scenarios – using a controlled component, such as TextInput and a global state.

Controlled vs. uncontrolled components

Let’s start with the first one. Almost every React Native application contains at least one TextInput that is controlled by the component state as per the following snippet.

Read more: https://snack.expo.io/q75wcVYnE

The above code sample will work in most of the cases. However, on slow devices, and in a situation where a user is typing really fast it may cause a problem with view updates.

The reason for that is simple – React Native’s asynchronous nature. To better understand what is going on here, let’s take a look first at the order of standard operations that occur while user is typing and populating your <TextInput /> with new characters.

Diagram that shows what happens while typing TEST

As soon as user starts inputting a new character to the native input, an update is being sent to React Native via onChangeText prop (operation 1 on the diagram). React processes that information and updates its state accordingly by calling setState. Next, a typical controlled component synchronizes its JavaScript value with the native component value (operation 2 on the diagram).

The benefit of such an approach is simple. React is a source of truth that dictates the value of your inputs. This technique lets you alter the user input as it happens, by e.g. performing validation, masking it or completely modifying.

Unfortunately, the above approach, while being ultimately cleaner and more compliant with the way React works, has one downside. It is most noticeable when there is limited resources available and / or user is typing at a very high rate.

Diagram that shows what happens while typing TEST too fast

When the updates via onChangeText arrive before React Native synchronized each of them back, the interface will start flickering. The first update (operation 1 and operation 2) performs without issues as the user starts typing T.

Next, operation 3 arrives, followed by another update (operation 4). The user typed E & S while React Native was busy doing something else, delaying the synchronization of the E letter (operation 5). As a result, the native input will change its value temporarily back from TES to TE.

Now, the user was typing fast enough to actually enter another character when the value of the text input was set to TE for a second. As a result, another update arrived (operation 6), with the value of TET.
This wasn’t intentional – the user wasn’t expecting the value of its input to change from TES to TE.

Finally, operation 7 synchronized the input back to the correct input received from the user a few characters before (operation 4 informed us about TES). Unfortunately, it was quickly overwritten by another update (operation 8), which synchronized the value to TET – final value of the input.

The root cause of this situation lies in the order of operations. If the operation 5 was executed before operation 4, things would have run smoothly. Also, if the user didn’t type T when the value was TE instead of TES, the interface would flicker but the input value would remain correct.

One of the solutions for the synchronization problem is to remove value prop from TextInput entirely. As a result, the data will flow only one way, from native to the JavaScript side, eliminating the synchronization issues that were described earlier.

Read more: https://snack.expo.io/DYMECpVPQ

However, as pointed out by @nparashuram in his YouTube video (which is a great resource to learn more about React Native performance), that workaround alone isn’t enough in some cases. For example, when performing input validation or masking, you still need to control the data that the user is typing and alter what ends up being displayed within the TextInput. React Native team is well aware of this limitation and is currently working on the new re-architecture that is going to resolve this problem as well.

Global state

Another common reason of performance issues is how components are dependent on the application global state. The worst-case scenario is when state change of single control like TextInput or CheckBox propagates render of the whole application. The reason for this is a bad global state management design.

We recommend using specialized libraries like Redux or Overmind.js to handle your state management in a more optimized way.

First, your state management library should take care of updating components only when a defined subset of data had changed – this is the default behavior of redux connect function.

Second, if your component uses data in a different shape than what is stored in your state, it may re-render, even if there is no real data change. To avoid this situation, you can implement a selector that would memoize the result of derivation until the set of passed dependencies will change.

A typical example of selectors with redux state management library

Common bad performance practice is a belief that the state management library can be replaced with the usage of custom implementation that is based on React Context. It may be handy at the beginning because it reduces boilerplate code that states manage libraries introduce. But using this mechanism without proper memoization will lead to huge performance drawbacks. You will probably end up refactoring state management to redux because it will turn out that is easier than the implementation of custom selectors mechanism to your current solution.

You can also optimize your application on a single component level. Simply using Pure Component instead of regular Component and using memo wrapper for function components will save you a lot of re-renders. It may not have an impact at first glance, but you will see the difference when non-memoized components are used in a list that shows a big set of data. It is usually enough for components optimizations.

Do not try to implement these techniques in advance, because such optimization is used rarely and in very specific cases.

Benefits: Your app works faster with fewer resources needed

You should always keep the performance of your app in the back of your head, but do not try to optimize everything in advance, because it usually not needed. You will end up wasting time on solving inexistent problems.

Most of the hard-to-solve performance issues are caused by bad architectural decisions around state management, so make sure it is well designed. Particular components should not introduce issues as long as you use Pure Component or memo wrapper.

After all, with all these steps in mind, your application should perform fewer operations and need smaller resources to complete its job. As a result, this should lead to lower battery usage and overall and more satisfaction from interacting with the interface.

Summary

Although UI re-renders are an extremely important part of the React Native optimization process, there is much more work to do. Having in mind the complexity of this process, next week we are going back with the next part of the guide describing dedicated higher-order components for certain layouts.

Stay tuned!

Callstack

We are the official Facebook partners on React Native. We’ve been working on React Native projects for over 5 years, delivering high-quality solutions for our clients and contributing greatly to the React Native ecosystem. Our Open Source projects help thousands of developers to cope with their challenges and make their work easier every day.

Contact us if you need help with cross-platform or React Native development. We will be happy to provide a free consultation.