6 TIPS YOU WANT TO KNOW ABOUT REACT NATIVE PERFORMANCE

React Native is fast by default, but it’s so easy to make it slower, especially on Android.

In my practice, I didn't see any JS developer who understands what's going on under the hood, so this article probably should help you to understand how to build react native application that is fast and optimize the performance of the existing ones.

How React Native works? 

  1. When the app starts up, the main thread starts execution and start loading JS.

  2. When JavaScript code has been loaded main thread sends it to another thread(JS Thread) to keep main thread response which is responsible for the UI. Having a separate thread for JavaScript is a good idea, because when JS does some heavy calculations freezing the thread for a while , the UI thread will not suffer.

  3. When React start rendering Reconciler starts "diffing", and when it generates a new virtual DOM(layout) it sends changes to another thread(Shadow thread).

  4. Shadow thread ("shadow" because it generates shadow nodes) calculates layout and then sends layout parameters/objects to the main(UI) thread

  5. Since only the main thread is able to render something on the screen, shadow thread should send generated layout to the main thread, and only then UI renders. 

NOTE

Every native module run in its own Thread Pool on iOS, though on Android all native modules run in one thread pool by default.


The 3 Parts

Generally speaking, we can separate React Native to 3 parts

  1. React Native - Native side

  2. React Native - JS side

  3. React Native - Bridge

Here lies one of the main keys to understanding React Native performance. Each side by itself is blazingly fast. The performance bottleneck often occurs when we move from the one realm to the other too much. In order to design a performant React Native app, you must keep passes over the bridge to a minimum.

 

Practical Optimization Tips

#1 Use shouldComponentUpdate

One of the most common issues is to not implement this lifecycle, diff state and props yourself for the first - see if your component should update or not, don’t pass too much unnecessary work to Reconciler, probably this will drop your JS thread's FPS. Though extend from PureComponent instead of Component

#2 Attach a key associated with your object

function UsersList() { 
  return ( 
    <View> 
        {this.state.users.map((user, i) => <User {...user} key={i} />)} 
    </View> 
  ); 
}

What happens when we sort, for instance, "users" array or add/remove a new user object to the array from the middle?

It will destroy rest User components and will create new components. But is there any need to do that? NO! Reconciler can’t find a way how to reuse existing components and that’s why it recreates many User components. It will send data to shadow thread through RN bridge - yoga will calculate much data again, and will send to UI, and it will render UI. To avoid similar performance issue - think what key are you give to your component? Is it attached to your data or not. So if you’ll give <User /> component user.id as key, it will reuse existing components, and just will move they directions, instead of destroying and creating, this will improve your list performance a lot.

function UsersList() { 
  return ( 
    <View> 
     {this.state.users.map((user, i) => <User {...user} key={user.id} />)} 
    </View> 
  ); 
}

#3 Get rid of ListView

Get rid of ListView component and use FlatList/SectionList/VirtualizedList instead. Every legacy react native app probably has a section where the ListView component is used. When scrolling down a long list, the memory footprint increases linearly and eventually exhausts all available memory. On a device as memory-constrained as a mobile device, this behavior can be a deal breaker for many. Right now React Native have better ScrollView implementations which I can call "Memory-Minded Lists". Changing your ListViews to FlatList can improve your app performance and will reduce memory usage.

#4 Use Native Navigation

Navigation is a skeleton of the app, so you need to pay much attention to it in order to have a good performance.

JS based navigation: There's a limit to how well you can fake navigation with pure JS and small details like side swipe for back get lost, tab navigation, push/pop etc.. Also consider what happens if Apple decides to revamp the design of the navigation bar like they did between iOS 6 and 7. Will we really go to the length of providing two separate experiences for different OS versions with the pure JS implementation? JS based solution is also bad for performance. Navigating between one screen to another may update third screen for no reason if you design a little bit wrong structure.    

Native Navigation: As in the web react-native uses one root view by default (RCTRootView on iOS and View on Android) from which view hierarchy of the react-native application starts. But with native navigation system every screen uses separate Root view. This means that instead of a single RCTRootView, app will have several ones running in parallel. Running screens in parallel gives us better performance. Additionally, native navigation approach is much more conservative in terms of memory usage compared to JS based navigation.

#5 Use "useNativeDriver" for animations where possible

React Native introduces great Animated library out of the box. But how it works ?

Let’s take a look at this example.

Animated.timing(this.state.translateX, {
  toValue: 10,
  duration: 1000
}).start()

this will do all calculations in JS thread - it means you can’t do any other calculations in JS during the animation. In most cases this limitation will hinder and will drop JS thread’s FPS. How to solve this problem?

The code above uses JS driver and here’s a breakdown of the steps for an animation and where it happens:

  1. JavaScript driver uses requestAnimationFrame to execute on every frame

  2. Intermediate values are calculated and passed to the View

  3. The View is updated using setNativeProps

  4. JS sends this changes to the native environment through the bridge

  5. Native updates UIView or android.View

But started from 0.42 react native introduced a new way to pass all work to the native side. It doesn't freeze JS thread and doesn't drop UI FPS, so you get smooth animations. Is there anything else you need to make you happy :) ?

It’s super easy to pass all work to the native

Animated.timing(this.state.translateX, {
  toValue: 10,
  duration: 1000,
  useNativeDriver: true // <--- add this line
}).start()

How it works?
Here’s a breakdown of the steps for an animation and where it happens:

  1. The native animation driver uses CADisplayLink or android.view.Choreographer to execute on every frame
  2. Intermediate values are calculated and passed to a view
  3. The UIView/android.View is updated

No more JS thread and no more bridge which means faster animations!

#6 Do not place all bets on JS

JavaScript is fast, but Java/Objc/Swift are faster especially for heavy calculations. So if your app is getting too slow due to some calculations and you can’t see any easy way to do it in JS - probably you should do it in the native environment with some 10 lines of code.

How would you add a video processing ability to your app, you know, simple things like trimming, compressing and adding filters? Initially, we did it in the WebView using Canvas, since there wasn't any other way to do that in JS. The result was horrible because we couldn't retain FPS on the level. We got 5-10 frames per second, which is a total disaster.

If video trimming is a very easy task for Swift, why would you bother doing that in JS?

4 days of the development and finally we were able to trim/compress/filter videos natively and retain 60 FPS for UI and JS. The good new for you is that we open sourced it as a library and hosted on github.

THE MORAL

Learn native languages if you want to develop react native apps with better performance


Conclusion

React, with its concept of virtual-DOM, provides us with an excellent optimization out-of-the-box. Changes to our rendered components in JS are batched asynchronously with a smart diff algorithm — thus minimizing the amount of data sent over the bridge. Nevertheless, poor understanding of how things work behind the scene can cause dramatic performance issues.

If you experience performance issues, most probably you did something wrong with your code. Don't assume it will be fixed by itself or by some testing - check things out! So maybe the most important tip is fixing performance issues as soon as possible. When your code grows it will be much more harder to identify and fix performance issues.

In one of the coming articles I will be talking about how actually to measure react native application performance, so see you soon!

Bye!

Previous
Previous

HOW WE BECAME AGILE AT SIMPLY: TIPS TO MAKE AGILE WORK

Next
Next

SCRUM VS KANBAN? WHICH WAY TO GO?