Building Content-Rich Pages on React Native

Traveloka’s Journey on Developing and Optimizing Vacation Product

Vanya Deasy Safrina
Traveloka Engineering Blog
7 min readDec 4, 2018

--

Traveloka has been using React Native for over two years now. During that period of time, we have developed several B2B applications and some features using React Native on our hybrid Traveloka main application. One of those features is Vacation that has been released on September 2018 (v3.4).

Vacation is our first content-rich feature on Traveloka developed using React Native. Thus, prior to its release, we faced several issues, especially in terms of its performance. By writing this article, we hope that we could share our experience in performance optimization on React Native for a content-rich product.

A Glimpse of Vacation Product

Vacation product is an overarching storefront that enables showcase of all Traveloka products, including transportation, accommodation, and Local & Destination (L&D) products. The storefront is a content-rich platform that provides users with inspirational contents for endless exploration and discovery of relevant products for their needs. Through Vacation, we aim to be present for users at relevant times — be it during their travel planning stage, impromptu planning for their nights or weekends, or turning their free time into exciting discovery of new places.

Vacation consists of three main pages:
1. Main Page
2. Articles Page
3. Destinations Page

Major Performance Issues

We experienced several performance issues while developing Vacation features. These issues need to be solved separately. However, all these issues combined caused lags everywhere.

Android GPU Rendering comparison of old and optimized version of the product

As you can see on the image, the rendering task looks really bad before we spent any optimization efforts. We were too far from 60 fps limit bar and it did not only happen on first render. It continues to perform badly on every user interaction. Thus, we need to analyze the causes of this performance issue and how to solve it, which will be explained below.

List with big resolution images

Vacation consists of a lot of pages with a lot of big resolution images on each page. This should be handled correctly for the pages to have a good performance.

Placeholders everywhere!

Before doing any optimization efforts, we will get to see placeholders everywhere since the images being loaded have big resolution. Put apart mobile data that you need to spend to just see one image, the time it would require to show one image will significantly longer and pretty much killing the mood for content-based product. Nobody got time to see placeholder everywhere all the time.

For this issue, we had two optimizations i.e., shrinking the image to the required dimension and replacing the Image component to native image component.

Shrinking the images is needed most as even the author image that we put on 40dp is an originally 8k one. We use an image dynamic processing host so that we can request an image based on the size that is actually needed. Therefore, to utilize that feature, we wrapped the Image component to get its width and height that are defined on style props. Then, those values are used to transform the original image source to the shrunk one.

Though, there is one condition left. How about a responsive full-width image? It gets a little tricky here. We initially request the image with a small resolution to be rendered. From there, we can get its container width with onLayout props and the image’s width and height using Image.getSize to calculate its ratio. With those two values (container width and image ratio), we can calculate the width and height the image needed for.

The second optimization method is to replace the ReactNative.Image component to a native component. This arises from our early hypothesis that there should be somewhere in the ReactNative.Image component that slows things down, especially when it needs to handle relatively big images. Our biggest bet for that is in the bridge part. The performance of the ReactNative.Image is still acceptable for handling one single big image. However, when there are multiple images to load at the same time, the bridge is overloaded with streams of pixel data going back and forth between JavaScript and Native side.

With that assumption in mind, we thought it’d be pretty interesting to see the image being handled on the native side. We found a good library for this, react-native-fast-image (thanks to Dylan Vann). This library is fantastic that it even allows us to reduce the windowSize props of the FlatList without sacrificing the react native pre-rendered content mechanism to prevent blank content when scrolling.

ScrollView Usage for List

To render a list of items, we initially use ScrollView, instead of FlatList. However, using ScrollView in our case is not the right decision since it renders every item at once and our item is pretty heavy with images. So, we decided to use FlatList instead.

FlatList is an absolute delight! It allows you to render a subset of items, and on scroll, it will render the rest of the items. For a simple view, the performance of ScrollView and FlatList won’t even be noticeable. However, it works so well with our cases with endless scrolling which you can achieve by using onEndReached props.

The charm of FlatList is that there are several props that you can use for performance optimization. First, you can easily define how many items you want on initial render with initialNumToRender. Furthermore, you can define the maximum number of items rendered outside of visible areas with windowSize. Lastly, do not set removeClippedSubviews props to false since it will make the offscreen child views be rendered. These features we have mentioned help a lot since we render several images on each item, therefore the request of those images can be postponed until the actual item is shown on screen.

Unnecessary re-rendering

By default, React always re-renders a component every time its parent receive new props or changes its state by using setState. It has its own perks, such as simplicity. Everything is out of the box. However, it does come with a price.

In our case, opacity value of the header is defined by the scroll amount of the page. The only component that must change when scroll state changes is theHeader component. It uses opacity, which value depends on scrollY, as its style. However, with an implementation like the one below, all components including ListWithBigImages will be re-rendered.

It’s no big deal when unnecessary re-rendering happens on simple components but imagine that a long list with several big images on each item gets re-rendered every time scroll event happens. A bug like the images cannot be rendered well when users scroll the list can happen (it did in our case).

Here is when we could say, React.PureComponent FTW! By extending PureComponent instead of Component, ListWithBigImages will not re-render on scroll event since it will not be defined as necessary by the component. Basically, the difference between them is that Component does not implements shouldComponentUpdate while on the other hand, PureComponent implements it with a shallow prop and state comparison.

Nonetheless, you may need to consider using Component instead of PureComponent when your components contain complex data structures since it may produce false-negatives for deeper differences. By using Component instead, you can define your own shouldComponentUpdate for more fine-grained control.

Animations

We use animations in a lot of places, mostly for transitions and header style based on scroll event. An example for this is that we need to increase the opacity level on scroll down event and vice versa.

Take a look at the header opacity!

To have a great performance on animations using Animated API, we make sure to use the native driver. This allows the animations to be executed on the native side. It only needs to pass the bridge between JavaScript and Native once, instead of passing data back and forth on every frame. To do that, you only need to specify this props useNativeDriver: true when starting the animation.

Although it’s great to pass the animation to the native driver, based on React Native documentation, there is a caveat that not everything you can do with Animated is currently supported by the native driver.

Wrapping Up

After developing several features, we learn that each feature we develop has a different type of difficulties. Through developing Vacation feature, we did get performance issues. However, it’s not an impossible task to optimize it from various angles. For us, finding a way to improve the output of our first content-rich product on Traveloka has been such a fun experience that we could learn a lot from.

--

--