React Native Performance: Do and Don't

Written by maxired | Published 2019/05/31
Tech Story Tags: react | react-native | mobile-app-development | hackernoon-top-story | javascript

TLDRvia the TL;DR App

TL;DR — Do read the docs of React, React Native and every libraries you are using Using remote Images can be painful: use a library. Some patterns can be harmful, be careful with PureComponents and HOCS. Keep your libraries up to date. Give some love to your app and you users.

This article is based on two years of experience building Nelio React Native Mobile Apps, available on iOS and Android. It assumes you already have some experiences with React and or React Native. It is not strictly speaking dedicated to React Native, as some of the advices you will find below apply for a regular React application. It is also not an exhaustive guide to performance: you can apply all advices found here but still have a laggy app. Don't blame us ;-)

Nelio is a food delivery startup focusing on Craft Food, currently operating in Paris. When delivering quality good is your business, it impacts all aspects of your company. From the marketing message to your codebase. Basically, everybody here is focused to deliver the best User eXperience.

Performance is one of the topic which can change the overall perception of the service you deliver. To be honest, we have sometimes a hard time matching our and our customers expectation on that topic. This articles is based on what we learned so far during this journey. Mistakes that we did, and will not reproduce, or stuff that we learnt and actions that we took in responses.

React and React-Native Performance

Being a React developer for the last 4 years, all I can say is that I love it. I was more than happy when joining this React Native project and it matched what I expected in terms of learning curve: Having the same framework make it really easy to start developing your first components, but later on you will need to get a deeper understanding of React Native. Just as being an advanced React Web developer requires you to have an understanding of what is going on in the browser ;-)

Regarding, performance in React Native, this is the same:

Everything you know about React performance will apply in a React Native application

That being said, probably the first next step is to jump to React performance doc and later on React Native performance doc. These are great resources that I will not spend anytime copy-paste or rewrite here ;-) I will rather focus on specific patterns that we put in places or decided to avoid in order to improve performances.

I will also not spend any time arguing whether React Native is fast enough or not, whether you should move to flutter or go fully native. There are some great React Native Apps out-there. Here is how we try to be one of them.

Do provide UI Feedback

Performance is mainly about perception rather than measuring precisely how long a function take time to complete. More important that how long ?, you should ask yourself why ? and when ?.

It is commonly accepted that after a user interaction you should give feedback in 100ms. Keep in mind that this threshold is a barrier that you should not cross. Keep also in mind that it is never too early to give feedback.

There are different ways to give feedback to the user. In React Native, a great and simple solution is to use the TouchableOpacity component on all component which accept user interactions. This will give feedback to the user during the interaction and is a great way to indicate that something will occur after the interaction.

If the click lead to opening a new screen, then you should spend some time thinking about your data loading. Usually a good approach is to open your new screen as soon as possible, render the content that you already have, and display a loader and/or some placeholder component while the content is loading. This is a technic also called Skeleton Screens.

If your click lead to something else, such as adding an item to your cart, to favorites, or post a chat message , this very often will include a remote API call. In that case, you should when possible — which is most of the time — act as if the endpoint did already respond successfully. This pattern is called Optimistic UI and is also wide spread across the industry.

At Nelio we are using GraphQL with ReactApollo. ReactApollo with the optimisticResponse parameter has a great solution to implement this pattern. This can also be done in different manners when using redux.

Images

A big topic regarding performance and usability of your React Native apps is image. Coming from a web background, it was for me a bit of a surprise. When you think about it, the browser are doing a huge job dealing with images, speaking of downloading, caching, decoding, scaling and displaying them: all that happening in a streamed way.

Do use an Image Caching solution

React Native code offers an Image component, which does a great job at displaying a single image, but has some issues when dealing with a lots of them. Specifically, we had some flickering issues, and also when too many images are loaded in our apps, at some points, they just stop loading.

We switch to react-native-fast-image, and it seems we are not the only one.

Recently react-native-fast-image was downloaded 12% as often a react-native on NPM registry which is almost as Expo

Do load images the size you need them

React-native-fast-image solved lots of issues we had, but we still had some random crashes in our app that we noticed was linked to some images. After a quick check, we noticed that we asked our app to download, cache, and scale tens of Images, each weighing several hundred KiloBytes. We tried initially to solve this problem by putting some hard limit to images upload, but we were not happy with this solution. In any case, be careful about the amount and size of the images your are loading, as this put lots of pressure on the device. A good solution is to do a big part of the job not on the device but before.

An image resized to specific dimensions with cloudImg with the following URL https://acoigzuuen.cloudimg.io/crop/640x360/q95/_s3_nelio_/kaviari-3384d944c6f8418679baee23-1523374450958.jpeg

Even if you don't have memory issues with images, it is still a good idea to size your image in the exact dimension you will display them. This will put less pressure on your user device.

We recently switch to use an Images Scaling CDN solution which would allows us to download images scaled precisely at the size we would display it. For the record we opted for CloudImage, and are currently very happy with it. It allow us to request an image on the fly with specific dimension. For a quick implementation with opted-in for a change of our GraphQL resolvers to translate to images URL to the cloudImage one. This change can also be done client side. CloudImage is the solution we went for, for could have used Cloudinary or hosting an open source solution such as imgProxy or Thumbor.

Do use Pure Components wisely

As said previously, React Native apps are before and after all React apps. Most advices which are good for React apps are good for React Native apps.

Probably the most popular advices around React performances concerns the use or not of PureComponent (and/or React.memo()). Long story short, useless re-render in React is usually not an issue but can become one on complex application. PureComponent is a way to prevent a component to re-render when his props did not changed. More precisely it implements it implements shouldComponentUpdate by doing a shallow comparison of props.

Some people think it is a good pattern to use PureComponent by default when implementing a new Component. My advice is to always keep in mind that it can be more harmful than helpful. This is really good example of Evil Premature Optimization.

If you need/really want to reduce re-render keep in mind, in that case, when instancing a component, you should not create any props of that PureComponent in your parent render method.

Don't create any new props at render time when using a pure component

The two main uses cases when giving props are providing new objects and new functions. The upper example also show providing a child Component as Children Props, but if you go beyond JSX, at the end, it it just a regular JS object.

Regarding objects, also keep in mind that arrays are objects. When switching your component to a more functional style, be always aware that most functional function, which are in themselves pure, create new references. Once again, don't call theses functions at render time.

Also avoid pitfalls when going functional

Another pattern that you might use a lot when using React-Native are renderProps: providing as props a function that return a component. From a props point of view, a render props is just a regular function provided as props, and so the general advice apply: Don't create renderProps at runtime.

Here at Nelio we don't use yet React Hooks which you will need to check if you haven't. Hooks shipped in React Native 0.59, and there will be a dedicated section about it below. What we use instead is recompose, from whose hooks have been greatly inspired.

Recompose, thanks to Pure , withHandlers and withPropsOnChange has proven for us to be a great utility at helping keeping our code base clean and improve performance.

Don't use HOCs on the fly

When you application becomes complex and you want share some common pattern across your component, it is not rare to use High Order Components.

Using High Order Component is rather a good practice even if this sometimes can be arguable as it increase indirection. It can though increases the complexity of your code comprehension.

Don't instance HOC at render

What you should really be careful is not instance any High Order Component on the fly, specifically during a render method. The reason why you don't want to do that is that you are effectively creating a new component. React can't know that this is effectively the same component as before (as this is indeed not). This put lots of pressure on the reconciliation algorithm. More than that, it also force React to run all lifecycle method, on the whole tree.

Maybe all of this is really obvious to you. Maybe you think this would be really dumb to do so, and even ask yourself why on earth did we think at some point it could be a good idea. If so, you can have a look at our currently open position as we are always eager to learn from the best.

I can remember one case where we had this bad pattern, noticed it and solved it. Basically this happened when we mixed the used of RenderProps and High Order Components. As Apollo user, we use in most places Apollo Query Component to fetch data from our endpoint. Because our code style is to use recompose when possible, the initial implementation was wrapping the Apollo Query component in a fromRenderProps HOC. This works great for all query where you don't need dynamic variables, but failed otherwise, as there is no way to give specific props to your render props component. The way we implemented this originally was wrong. We later identifier two ways to improve that. The first one was to accept to not use Recompose HOC here, and just do a regular Component using a more classical implementation. The second one, that we choose, was to switch to Apollo graphQL HOC, which is documented to match our usage.

Another usage that I can imagine is if you need to instance a High Order Component based on props that you received. I created a small example that show such case. We had in our codebase something that was close from this pattern. After some thinking, we get rid of it by either by not using HOC in those places, and replace it by renderProps or directly providing the Component as props.

Do use another pattern such as Children, renderProps, or Component as Props

Don't implement bulk reducers

If you are not using GraphQL, chances are you are using redux. Here we are using both, but I don't recommend it for most cases.

If you are not using redux with normalizr and/or rematch, or at some point writing yourself your reducers, please be careful to always mutate only the objects that you need to. If you followed attentively Redux basic tutorial you are already aware of this point, but if you are not, then read it again ;-)

If you are like us and sometime too eager to code, then maybe when refetching a list of item from the network, and saving it to your reducer — even in a normalized way — you naïvely do :

<a href="https://medium.com/media/3d898468db6f5adc84804d660cd84d87/href">https://medium.com/media/3d898468db6f5adc84804d660cd84d87/href</a>

If doing so and having some performances issues while rendering a list of items which can be refreshed in the background, then what you will want to do is update your store only when needed. More precisely only update references which need to be: if an items as the same value as before, then you probably don't need to save a new reference for it in redux. Updating reference in your store, will create useless render which by the end will produce the same components.

Don't naively share Selectors

If using redux, you are using mapStateToProps functions in your connect() call.

As your application grow, and your mapStateToProps complexity increase, then you might notice that some of the computation that you do in your mapStateToProps become heavy. You might also notice that you are sometimes re-rendering too much, which might seems counter intuitive as connect implements his own props shallow comparison.

The issue here is basically the same than with pureComponent. If every time you render your parent component you give new props to a pureComponent, it will need to re-render. What you want do here is keep the prevProps if the behavior of your component will be the same.

Once this is correctly understood, then reselect is a great solution to overcome this issue. This once again introduce a little boilerplate but this is definitely worth-it.

But be careful, a bad use of reselect can also introduce new performance issue. This is specially the case when you share your reducers in different places of you app, or shared across different instances of the same component. The problem here is that reselect selectors are basically memoized method whose cache has with limit of one. Reselect documentation has a dedicated section concerning shared selectors and provide a pattern to overcome it. Basically it consists of creating new selectors for each instance of the component. Others library such as re-reselect solves this issue differently.

In any case, you should be really careful about this when you start to share you selector across Components or different instances of the same component.

Going further

Delivering a lag free experience on all smartphones is not a one time tasks, but as often with Software Development, a continuous quest. You will find below some improvements that we are currently considering.

Use React Native 0.59

As said previously, RN0.59 bring React hooks to React Native. Using hooks should allow us to switch away from recompose, which is not developed anymore.

Moreover, React Native 0.59 bring to Android devices an upgraded version of JavaScriptCore, which have not been updated in a while. This update bring a performance gain of about 25% and 64 bits supports which will be mandatory on Google Play store starting August 1, 2019.

Experiment with FlatList Options

When rendering a list of elements, you should always use a component based on a VirtualizedList, such as a FlatList or a SectionList. Depending of the number of items of your list, the complexity of your component and their dimensions, you probably want spend some time tweaking their props, as lots of them will have a direct impact on Performance.

Use Tool to detect your performance issue

When trying to understand performance issues, you want try to understand how much time a component isMounting or rendering. React Profiler has been a great help at understanding sources of lags.

Something that we haven't used yet but can be useful when trying to find sources of responsiveness in your app, is spying the queue, as explained here. The Queue is the communication proxy channel between the JS part of you app and the Native Part.

Your interface might be something half reactive : For example the scroll view or the touchable opacity work, but then the JS handler that you wrote are not triggered. In that case it means that the native code is executed, but not the JS. Spying if the queue is super busy is probably a good step to understand what is the source of your issue.

Thanks

Woo, congrats for reading until there. Any personal tips to share ? Don't be shy and comment ;-) Any question you still have ? Please put it there, and I'll do my best to answer.


Written by maxired | Software developer building awesome products
Published by HackerNoon on 2019/05/31