blog content
The following article is part of The Ultimate Guide to React Native Optimization and explains how to find balance between native and JavaScript.
Why is it important?
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.
In other blog posts based on The Ultimate Guide to React Native Optimization, we touch on the following topics related to improving performance through understanding the React Native implementation details:
- Picking the right external libraries for your app
- Optimizing battery drain with mobile-dedicated libraries
- Animating at 60FPS
- Improving React Native performance with high-order components
- Optimizing your React Native app’s JavaScript bundle
Be sure to check them out. Now let’s move to our main topic.
Finding the balance between native code and JavaScript
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 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.
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 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, you will likely have a dropped frame - the certain operation wasn’t performed causing jitters in the UI.
Certain libraries, such as <rte-code>Animated<rte-code> provide special workarounds. I - 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 keeping 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 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.
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, 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. Remember 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.
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.
React Native uses StyleSheet API to pass styles over the bridge most of the time. That API processes your styles and ensure they’re passed only once over the bridge. During runtime, it substitutes the value of the 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.
Balance between native and JavaScript makes codebase 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 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 article 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.
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 - 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 getting specialized in Objective-C or Java.
Need help with performance optimization?
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.