Find the harmony between native and JavaScript to build fast-working and easy-to-maintain apps

An introduction

The following article is a part of The Ultimate Guide to React Native Optimization and describes the fourth point of this guide: finding the balance between native and JavaScript.

Why is it important?
Developing with React Native may help you to create a fast-working and easy-to-maintain apps. But what you need to remember that it’s essential to find the balance between native and JavaScript to make your app work as good as possible. If you won’t do it, your app will slow down significantly because, in simple words, it will be overloaded. Having in mind that it can harm the performance of your products, we decided to share with you some best practices on how to find that harmony.

In previous parts of our guide, we have discussed:

Be sure, to check them out.

Now, let’s move to our main topic.

Find the balance between native and JavaScript

Issue: while working on the 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 3rd party SDK that doesn’t have an 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. React Native builds on top of an abstraction called bridge, which provides bidirectional communication between JavaScript and native worlds. As a result, JavaScript can execute native APIs and pass the necessary context to receive the desired return value. That 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 be up for another task already.

diagram that shows the communication between the bridge and React Native and the JSON serialization

The number of JavaScript calls that arrive over to 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 your 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 certain operation wasn’t performed causing jitters in the UI.

React Native frames

Certain libraries, such as Animated provide special workarounds – in this case, use NativeDriver – 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 is 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 it 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 such as invalid arguments, that 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.

Bypassing arguments to native module

In case of an incorrect or missing parameter, the native module is likely to throw an exception. The current version of React Native 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 in the next tick when the native side processes the call and receives an exception from the native module.

In such a simple scenario, you have lost a bit of time waiting for the exception that you could’ve checked for beforehand.

Using native module with arguments validation

The above is not only tied to the native modules itself. 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 – just like you execute your native method with the JavaScript arguments.

To put this into better perspective, let’s take a closer look at styling within React Native apps.

Read more: https://snack.expo.io/7H5S504j3

The easiest way to style a component is to pass it an object with styles. While it works, you will not see it happening too much. It is generally considered an anti-pattern, unless you’re dealing with dynamic values, such as changing the style of the component based on the state.

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

React Native uses StyleSheet API to pass styles over the bridge most of the time. That API processes your styles and makes sure they’re passed only once over the bridge. During runtime, it substitutes the value of style prop with a numeric unique identifier that corresponds to the cached style on the native side.

As a result, rather than sending a large array of objects every time React Native is to re-render its UI, the bridge has to now deal with an array of numbers, which is much easier to process and transfer.

Benefits: The codebase is faster and easier to maintain

Whether you’re facing any performance challenges right now, it is a good practice to implement a set of best practices around native modules as the benefits are not just about the speed but also the user experience.

traffic light symbolizing the traffic on the bridge between native and javascript

Sure, keeping the right amount of the traffic flowing over the bridge will eventually contribute to your application performing better and working smoothly. As you can see, certain techniques mentioned in this section are already being actively used inside React Native to provide you a satisfactory performance out of the box. Being aware of them will help you create applications that perform better under heavy load.

However, 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 straight copy-paste from the documentation – very simple and easy to understand.

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 getting specialized in Objective-C or Java.

Summary

To sum up, keeping the right balance between native and JavaScript allows you to build apps that work fast and are easier to maintain. In consequence, by implementing the aforementioned practices in your projects, you will be able to reduce spendings for this process.

Note: If you would like to find out more about app maintenance costs, read this article.

In the next article, we are going to give you some practices to achieve animations at 60FPS in your apps.

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.