How to Choose Tools Wisely and Build Fast-Working React Native Apps

In short

This article discusses the importance of finding the right balance between native code and JavaScript when developing with React Native to ensure optimal app performance. It highlights the impact of efficient communication between the two realms, the need for proper argument validation, and the advantages of maintaining a thin native layer for faster and easier codebase maintenance.

Originally published in December 2021, updated in March 2024.

React Native may allow you to create fast-working and easy-to-maintain apps. But to make that happen, you need to first find the balance between native and JavaScript. Otherwise, your app can slow down significantly because, in simple words, it will be overloaded. Since any harm to app performance equals damage to business, we decided to share with you some best practices on finding that harmony.

Find the balance between native and JavaScript to build fast-working and low-maintenance apps

Issue: While working on native modules, you draw the line in the wrong place between native and JavaScript abstractions

When working with React Native, you're going to be developing JavaScript most of the time. However, there are situations when you need to write a bit of native code. For example, you're working with a third-party SDK that doesn't have official React Native support yet. In that case, you need to create a native module that wraps the underlying native methods and exports them to the React Native realm.

All native methods need real-world arguments to work. ReactNative builds on top of an abstraction called a bridge, which provides bidirectional communication between JavaScript and native worlds.

Note: There's an ongoing effort to move away from asynchronous bridge communication to a synchronous one.

As a result, JavaScript can execute native APIs and pass the necessary context to receive the desired return value. The communication itself is asynchronous – it means that while the caller is waiting for the results to arrive from the native side, the JavaScript is still running and may already be up for another task.

communication between the bridge react native and JSON

The number of JavaScript calls that arrive over the bridge is not deterministic and can vary over time, depending on the number of interactions that you do within your application. Additionally, each call takes time, as the JavaScript arguments need to be stringified into JSON, which is the established format that can be understood by these two realms.

For example, when the bridge is busy processing the data, another call will have to block and wait. If that interaction was related to gestures and animations, it is very likely that you have a dropped frame – the operation wasn't performed causing jitters in the UI.

React Native frames

Certain libraries, such as <rte-code>Animated<rte-code> provide special workarounds. In this case, use <rte-code>NativeDriver<rte-code>, which serializes the animation, passes it once upfront to the native thread and doesn’t cross the bridge while the animation is running – preventing it from being subject to accidental frame drops while another kind of work is happening.

That’s why it's important to keep the bridge communication efficient and fast.

More traffic flowing over the bridge means less space for other things

Passing more traffic over the bridge means that there is less space for other important things that React Native may want to transfer at that time. As a result, your application may become unresponsive to gestures or other interactions while you're performing native calls.

If you are seeing a degraded UI performance while executing certain native calls over the bridge or seeing substantial CPU consumption, you should take a closer look at what you are doing with the external libraries. It is very likely that there is more being transferred than there should be.

Solution: Use the right amount of abstraction on the JS side – validate and check types ahead of time

When building a native module, it is tempting to proxy the call immediately to the native side and let it do the rest. However, there are cases (like invalid arguments) which end up causing an unnecessary round-trip over the bridge only to learn that we didn’t provide the correct set of arguments.

Let’s take a simple JavaScript module that does nothing more but proxies the call straight to the underlying native module.

In the case of an incorrect or missing parameter, the native module is likely to throw an exception. The current version of ReactNative doesn't provide an abstraction for ensuring the JavaScript parameters and the ones needed by your native code are in sync. Your call will be serialized to JSON, transferred to the native side, and executed.

That operation will perform without any issues, even though we haven't passed the complete list of arguments needed for it to work. The error will arrive when the native side processes the call and receives an exception from the native module.

In such a scenario, you have lost some time waiting for the exception that you could've checked for beforehand.

The above is not only tied to the native modules themselves. It is worth keeping in mind that every React Native primitive component has its native equivalent and component props are passed over the bridge every time there's a rendering happening – or is it? It's not always the case when a component re-renders. ReactNative renderer is smart enough to diff the parts of our JSReact component hierarchy and only send enough information through the bridge so that the native view hierarchy is updated.

This is the case when styling components like e.g. View or Text using the <rte-code>style<rte-code> prop. Let's take a look at the following example using inline styles.

Even though the <rte-code>style<rte-code> prop is passed as an inline object, it doesn't cause us any performance issues. Neither when we dynamically change the styles based on props, nor when we re-render the App component. View passes its props almost directly to the underlying native representation. And thanks to the ReactNative renderer, no matter how often we re-render this component on the JS side, only the smallest amount of data necessary to update the style prop will be passed through the bridge.

In React Native we have nicer ways to deal with styling and it's through StyleSheet API – a dedicated abstraction similar to CSS StyleSheets. Although it provides no performance benefits, it's worth calling it out for the ease of development and maintenance. When we develop our app in TypeScript or Flow, StyleSheet is well typed and makes it possible for our code editors to auto-complete.

The codebase is faster and easier to maintain

Whether you're facing any performance challenges right now, it is smart to implement a set of best practices around native modules as the benefits are not just about the speed but also the user experience. Sure, keeping the right amount of traffic flowing over the bridge will eventually contribute to your application performing better and working smoothly.

As you can see, certain techniques mentioned here are already being actively used inside React Native to provide you with a satisfactory performance out of the box. Being aware of them will help you create applications that perform better under a heavy load.

One additional benefit that is worth pointing out is the maintenance.

Keeping the heavy and advanced abstractions, such as validation, on the JavaScript side will result in a very thin native layer that is nothing more but just a wrapper around an underlying native SDK. In other words, the native part of your module is going to look more like a copy-paste from the documentation – comprehensible and specific.

Mastering this approach to the development of native modules is why a lot of JavaScript developers can easily extend their applications with additional functionality without specializing in Objective-C or Java.

Need help with performance optimization?

We are the official Meta partners on React Native. We’ve been working on React Native projects for years, delivering high-quality solutions for our clients and contributing greatly to the React Native ecosystem. Our Open Source projects help thousands of developers cope with their challenges and make their work easier every day. Contact us if you need help with cross-platform or React Native development.

FAQ

No items found.
React Galaxy City
Get our newsletter

By subscribing to the newsletter, you give us consent to use your email address to deliver curated content. We will process your email address until you unsubscribe or otherwise object to the processing of your personal data for marketing purposes. You can unsubscribe or exercise other privacy rights at any time. For details, visit our Privacy Policy.

Callstack astronaut
Download our ebook

I agree to receive electronic communications By checking any of the boxes, you give us consent to use your email address for our direct marketing purposes, including the latest tech & biz updates. We will process your email address and names (if you have entered them into the above form) until you withdraw your consent to the processing of your names, or unsubscribe, or otherwise object to the processing of your personal data for marketing purposes. You can unsubscribe or exercise other privacy rights at any time. For details, visit our Privacy Policy.

By pressing the “Download” button, you give us consent to use your email address to send you a copy of the Ultimate Guide to React Native Optimization.