Playing with a real life use-case

See the swipeable card on the left? It’s a popular mobile UX pattern used in apps like Google Now.

It’s also surprisingly interesting to implement, performance-wise.

We’re going to implement this example multiple times and see the performance implications of each approach.

It makes sense to create a reusable component Swipeable that adds the swipe behavior (x translation and opacity change) to any content component we give it as child — Card in this case.

Our first implementation — PanResponder

Let’s start with the straightforward approach. Since we want to listen on touch gestures, we’ll use React Native’s PanResponder. Every time we receive a move event, we’ll calculate the new opacity and x translation based on the total horizontal distance traveled, and update them using local state:

First implementation

What performance should we expect from this approach? Let’s remember the guideline stated earlier — In order to architect performant React Native apps, we must keep passes over the bridge to a minimum.

It seems that this example implementation is doing the exact opposite. Touch events originate in the native realm, since that’s where the device tracks the user’s finger. Our updates to the component’s state obviously happen in the JS realm. This is not normally a major issue, the problem here is that these updates take place on every frame! This means that for every single animation frame, where we want things to feel most fluid, data must pass over the bridge.

This is a performance bottleneck that pure native apps don’t have, making it much easier for them to reach the holy grail of 60 FPS, especially on weaker devices, and especially in real life cases that are a little more complicated than this example.

Didn’t I read something in the docs about Direct Manipulation?

If you care about performance, you’ve probably read the docs cover-to-cover and vaguely remember this article about direct manipulation of components.

Sounds promising, let’s update the native component directly and improve performance! We’ll give it a try, here is the implementation:

Second implementation

Did this solve our bridge performance issues? Not really — since we’re still updating from the JS realm. But it did optimize something worth understanding. In the previous implementation, on every frame we didn’t just send data over the bridge, we also re-rendered our component. In this specific case, the render function barely does anything so this wasn’t an issue. But what if our render function was more complex and computationally-intensive?

Traditionally, in order to update a React component, we have to re-render. If our update is very localized, like changing a specific style (x translation and opacity) we can surgically make it directly without the full render and reconciliation. This goes against the React “state of mind” so it’s best not to do this often and limit ourselves to use-cases where we have a specific property changing very rapidly (eg. during an animation).

Can we get back to fixing the bridge issue?

One of the most beautiful things about React Native is that we can take any piece of our codebase and move it seamlessly to native — even just a single component.

Developers often mistake React Native as a pure JS environment — it isn’t. It is true that JS would often give the best developer experience, but there are cases where native gives a superior user experience. I urge you, if you come from a web background — don’t fear native. It’s another tool in your belt which usually takes the same amount of stackoverflowing to exercise.

Since touch events originate in the native realm, what would happen if we do our x translation and opacity updates in native as well? Take a look:

Third implementation in Objective-C

The only part we’ve moved to native is the Swipeable container component. This would guarantee ourselves 60 FPS and it seems that the code is actually shorter. Notice that our Card content components remained in pure-JS. Here is how our native class is used inside our JS layout:

The future of React Native

While it is true that we can use native code selectively to plug our performance holes, the future of the framework is to improve and make sure we need to do so less and less.

It is possible to design clever JS interfaces that would minimize passes over the bridge and reach the same results. What if in our example, our JS code didn’t have to update the native realm on every frame? What if we could just specify once, in the beginning of the interaction, which properties are locked to which native event, and let some native module in the inner belly of React Native offload the updates for us? This would make us pass over the bridge just once — in the beginning.

React Native is evolving in this direction, and one of the primary treats we’ve been given is the new Animated library. Let’s implement our example for the fourth and last time with Animated in pure JS:

Fourth implementation in JS

As you can see, the Animated library treats animations and interactions in a very declarative way. If we can declare how an interaction behaves, this declaration can be serialized and sent itself over the bridge. This opens the possibility for a generic native module to process the interaction for us and offload the frame by frame updates.

Unfortunately, the current (June 2016) implementation of Animated doesn’t offload everything to native yet. This means that our fourth implementation currently still suffers from the same bridge bottleneck. Having said that, progress is being made and I’m confident that future versions will allow us to overcome the bridge limitation from JS in many cases.

Comparing all four implementations

Reading about performance isn’t the same as feeling it in real life. You can play with fully working versions of the four implementations in the following repo, presented side-by-side for easy comparison:

Please run the example on an actual device since the simulator doesn’t give authentic results. In addition, the repo contains optional flags to simulate stress conditions in the app — such as bursts of activity over the bridge and computationally-heavier render functions. It’s interesting to examine how the fluidity of each implementation changes under these conditions.

Conclusion and parting words

Developing mobile apps in React Native is awesome, but convenience sometimes comes at a price. It is possible though to mitigate almost every performance issue, and the key is understanding what goes on under the hood.

At Wix.com, we are obsessive about UX and delivering the native user experience mobile users have come to expect. Here’s our rough guideline for obsessive React Native performance: