The following article is focused on the second point of The Ultimate Guide to React Native Optimization tactics: using dedicated components for certain layouts.
Why do we consider this aspect important?
Using specialized components will always make your app work as fast as possible. It has a significant impact on the performance of your app which leads to better user experience.
Going further, by applying the practices described in the following article, you will increase the revenue-generating efficiency of your products and increase their ROI.
What’s more, in this article we will tell you how to save yourself a lot of time reimplementing the most common UI patterns from the ground up. Thanks to that, you will be able to implement future updates faster.
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
Now let’s find out how to use higher-order React Native components to achieve the best possible performance of your apps.
Primitive and high-order components in React Native
In a React Native application, everything is a component. At the end of the component hierarchy, there are so-called primitive components, such as <rte-code> Text<rte-code> , <rte-code> View<rte-code> , or <rte-code> TextInput<rte-code> . These components are implemented by React Native and provided by the platform you are targeting to support the most basic user interactions.
When we’re building our application, we compose it out of smaller building blocks. To do so, we use primitive components. For example, in order to create a login screen, we would use a series of <rte-code> TextInput<rte-code> components to register user details and a <rte-code> Touchable<rte-code> component to handle user interaction. This approach is true from the very first component that we create within our application, and holds true throughout the final stage of its development.
On top of primitive components, React Native ships with a set of higher-order components designed and optimized to serve a certain purpose. Being unaware of them or not using them in all the places can potentially affect your application performance, especially as you populate your state with real production data. Bad performance of your app may seriously harm the user experience. In consequence, it can make your clients unsatisfied with your product and turn them towards your competitors.
How high-order components affect your React Native app’s performance
If you’re not using specialized components, you are opting out of performance improvements and risking degraded user experience when your application enters production. It is worth noting that certain issues remain unnoticed while the application is developed, as mocked data is usually small and doesn’t reflect the size of a production database.
Specialized components are more comprehensive and have broader API to cover the vast majority of mobile scenarios.
Always use specialized component, e.g. FlatList for lists
Let’s take long lists as an example. Every application contains a list at some point. The fastest and dirtiest way to create a list of elements would be to combine <rte-code>ScrollView<rte-code> and <rte-code>View<rte-code> primitive components.
However, such an example would quickly get into trouble when the data grows. Dealing with the large data-sets, infinite scrolling, and memory management was the motivation behind <rte-code>FlatList<rte-code>, - a dedicated component in React Native for displaying and working with data structures like this.
Compare the performance of adding new list element based on <rte-code>ScrollView<rte-code>
to list based on <rte-code>FlatList<rte-code>.
The difference is significant, isn’t it? In the provided example of 5000 list items, <rte-code>ScrollView<rte-code> version does not even scroll smoothly.
At the end of the day, <rte-code>FlatList<rte-code> uses <rte-code>ScrollView<rte-code> and <rte-code>View<rte-code> components as well - what’s the deal then?
Just using <rte-code>FlatList<rte-code> may not be enough in some cases. <rte-code>FlatList<rte-code> performance optimizations relay on not rendering elements that are currently not displayed on the screen. The most costly part of the process is layout measuring. <rte-code>FlatList<rte-code> has to measure your layout to determine how much space in the scroll area should be reserved for upcoming elements.
For complex list elements, it may slow down interaction with a flat list significantly. Every time <rte-code>FlatList<rte-code> will approach to render the next batch of data, it will have to wait for all new items to render to measure their height.
However, you can implement <rte-code>getItemHeight()<rte-code> to define element height up-front without the need for measurement. It is not straightforward for items without constant height. You can calculate the value based on the number of lines of text and other layout constraints. We recommend using <rte-code>react-native-text-size<rte-code> library to calculate the height of the displayed text for all list items at once. In our case, it significantly improved responsiveness for scroll events of <rte-code>FlatList<rte-code> Android.
FlashList as a successor to FlatList
As already discussed, <rte-code>FlatList<rte-code> drastically improves the performance of a huge list compared to <rte-code>ScrollView<rte-code>. Despite proving itself as a performant solution, it has some caveats.
There are popular cases where developers or users have encountered, for instance, blank spaces while scrolling, laggy scrolling, and a list not being snappy, almost on a daily basis. <rte-code>FlatList<rte-code> is designed to keep certain elements in memory, which adds overhead on the device and eventually slows the list down, and blank areas happen when <rte-code>FlatList<rte-code> fails to render the items fast enough.
We can, however, minimize these problems to some extent by following the tips here, but still, in most cases, we want more smoothness and snappy lists. With <rte-code>FlatList<rte-code>, the JS thread is busy most of the time, and we always fancy having that 60FPS tag associated with our JS thread when we’re scrolling the list.
So how should we approach such issues? If not <rte-code>FlatList<rte-code>, then what? Luckily for us, the folks at Shopify developed a pretty good drop-in replacement for <rte-code>FlatList<rte-code>, known as <rte-code>FlashList<rte-code>. The library works on top of RecyclerListView, leveraging its recycling capability and fixing common pain points such as complicated API, using cells with dynamic heights, or first render layout inconsistencies.
<rte-code>FlashList<rte-code> recycles the views outside the viewport and re-uses them for other items. If the list has different items, <rte-code>FlashList<rte-code> uses a recycle pool to use the item based on its type. It’s crucial to keep the list items as light as possible, without any side effects; otherwise, it will hurt the list's performance.
There are a couple of props that are quite important with <rte-code>FlashList<rte-code>. First is <rte-code>estimatedItemSize<rte-code>, the approximate size of the list item. It helps <rte-code>FlashList<rte-code> to decide how many items to render before the initial load and while scrolling. If we have different-sized items, we can average them. We can get this value in a warning by the list if we do not supply it on the first render and then use it forward. The other way is to use the element inspector from the dev support in the React Native app.
The second prop is <rte-code>overrideItemLayout<rte-code>, which is prioritized over <rte-code>estimatedItemSize<rte-code>. If we have different-sized items and we know their sizes, it’s better to use them here instead of averaging them.
Let’s talk about measuring <rte-code>FlashList<rte-code>. Remember to turn on release mode for the JS bundle beforehand. <rte-code>FlashList<rte-code> can appear to be slower than <rte-code>FlatList<rte-code> in dev mode. The primary reason is a much smaller and fixed <rte-code>windowSize<rte-code> equivalent. We can leverage <rte-code>FlashList<rte-code>’s built-in call-back functions to measure the blank area <rte-code>onBlankArea<rte-code> and list load time <rte-code>onLoad<rte-code>. You can read more about available helpers in the Metrics section of the documentation.
We can also use Bamlab’s Android Performance Profiler, which gives us the results for FPS on the release builds in the form of a performance report. It also creates a nice-looking graph of CPU usage over the period of profiling, so we can verify how certain actions affect this metric.
There’s also a React Native Performance Monitor Flipper plugin. We can use it to measure the FPS of the JS and UI threads in debug builds. Make sure to turn off the DEV mode when profiling.
Your app works faster, displays complex data structures and you opt-in for further improvements
Thanks to using specialized components, your application will always run as fast as possible. You automatically opt in to all the performance optimizations done by React Native so far and subscribe for further updates to come.
At the same time, you also save yourself a lot of time reimplementing the most common UI patterns from the ground up. Sticky section headers, pull to refresh - you name it. These are already supported by default if you choose to go with <rte-code>FlatList<rte-code>.
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. We will be happy to provide a free consultation.