I’m working on my performance talk for , which is based on the post “ ”. I’ve decided to freshen up the example we’ll be discussing in order to walk through some exciting new API available today. React Amsterdam 2017 Performance Limitations of React Native and How to Overcome Them Some background In this post, we’ll be assuming you are already familiar with the basic architecture of — the , the and the connecting the two. If not, this topic is discussed in detail in the . React Native native realm JavaScript realm bridge previous post Let’s also remember the main conclusion from the previous post that will guide our performance-oriented discussion: In order to architect performant React Native apps, we must keep passes over the bridge to a minimum. So.. Time to jump into our new interesting use-case which, as it turns out, is not trivial to implement with React Native in a performant manner. A new real life example Our use-case is inspired by the home screen of the . The screen holds a feed of cards and contains a large header image welcoming the user to the app. This is what the UX looks like: Wix.com app Notice two interesting effects. The first — as the user is scrolling down the list of cards, the header image dissolves slowly into the gray background. The second — if the user scrolls up and is already at the top of the list, for the sake of continuity of movement the header image zooms slightly until it bounces back (this is called an ). overscroll effect Effects like these may seem minor, but are part of the that makes a magical user experience and separates the mediocre apps from the great ones. We’ll have to be careful with the implementation though. If it doesn’t run smoothly at 60 FPS, the effect might backfire and degrade user immersion instead of enhancing it. last mile The general component layout of the screen is very simple: Notice that the header image is not part of the ListView because it remains static and doesn’t scroll with the list content. Also notice we are still using the older implementation instead of the recommended . This is due to some extra ListView provides that isn’t yet available in FlatList. ListView FlatList flexibility Our first implementation — onScroll events in JavaScript As usual, we’ll start with the straightforward approach. If we want the effects to be tied to scroll position, we can listen on scroll events. Every time the scroll position changes, our event listener will fire and we’ll be able to re-render the header image and apply the appropriate effect. The setup is somewhat tricky because doesn’t expose the internal used to scroll the content. It works by allowing us to provide our own custom ScrollView using the prop. ListView ScrollView [renderScrollComponent](https://facebook.github.io/react-native/docs/listview.html#renderscrollcomponent) Applying the effects to the header image is as simple as defining style properties. We’ll use to control the dissolve effect and to control the zoom effect. We’ll store the values for both in local component state and re-render by calling . [opacity](https://facebook.github.io/react-native/docs/view.html#style) [transform.scale](http://facebook.github.io/react-native/releases/0.40/docs/transforms.html) setState This is the complete implementation: As you can see, on every event we decide which effect to apply based on scroll position ( ). If the scroll position is positive (downwards scroll), opacity is decreased from to . If the scroll position is negative (upwards overscroll), scale is increased from to . onScroll contentOffset 1.0 0.0 1.0 1.4 First attempt — performance analysis What performance should we expect from this implementation? The main performance bottleneck is probably the number of passes over the bridge. Let’s count passes by analyzing what’s running in the (purple on the left) and what’s running in the (black on the right): JavaScript realm native realm Just like any other view event, scroll events originate in the Our JavaScript logic runs in the . Once we re-render, the new view properties must be applied to the actual native views back in the . This means we’re passing over the bridge twice for every frame. native realm. JavaScript realm native realm On busy apps, this will prevent us from running at 60 FPS. Let’s improve. A second implementation attempt — native scroll listener React Native is very flexible regarding which components are implemented in JavaScript and which components are implemented in pure native. A native component can contain a JavaScript component and a JavaScript component can contain a native one. Whenever we have a performance issue, we can usually pull this rabbit out of the hat. Porting one of our components to native can usually solve the problem. Let’s try to apply this principle here. Our performance bottleneck stems from implementing our scroll listener in JavaScript. Since scroll events originate in the , executing our logic in JavaScript will always incur overhead. Let’s move the logic to native. native realm onScroll onScroll The purpose of is to update view properties (opacity and scale). We can save this trip over the bridge as well by closing the entire loop in native. Instead of changing the opacity and scale of the header image directly, we can wrap the image with a native container component —conveniently named — that will change its own properties. This will work because both opacity and scale of a container affect its children as well. This component will contain the native implementation of our logic. onScroll NativeWrapper NativeWrapper onScroll Our new layout: The only challenge so far is hooking our native scroll listener to the correct ScrollView. The ScrollView is part of the ListView and is neither a child or parent of our . We need to be able to pinpoint it from within the native implementation in order to connect the listener. Every React component has a — sometimes called a . This is just a number that uniquely identifies this component instance. We can find this number using and simply pass it as prop to . NativeWrapper node handle React tag [ReactNative.findNodeHandle](https://github.com/facebook/react/blob/72196da82915bee400edb1599d4223926aa2a8a0/src/renderers/native/findNodeHandle.js#L59) NativeWrapper Now that the JavaScript side is all set up, it’s time to get our hands dirty with some Objective-C. Porting the logic itself to Objective-C is easy. The code actually looks almost exactly the same. Instead of calling to update the view properties, we can simply change the view properties directly since we’re running in the . onScroll setState native realm The only challenge here once again is hooking our native scroll listener. Once the numeric of the ScrollView arrives via props, we need to translate it back to a view object reference in order to access the underlying ScrollView instance. This involves a little that uses the to do the translation. node handle borrowed boilerplate UIManager This is the Objective-C implementation: As you can see, the implementation is almost identical to the JavaScript one. If the scroll position is positive (downwards scroll), opacity is natively decreased from to . If the scroll position is negative (upwards overscroll), scale is natively increased from to . onScroll 1.0 0.0 1.0 1.4 Second attempt — performance analysis We expect performance to be much better since this implementation is tailored for reducing passes over the bridge. Let’s count passes by analyzing what’s running in the (purple on the left) and what’s running in the (black on the right): JavaScript realm native realm It’s not surprising that we’ve indeed eliminated all the passes over the bridge after initialization. Both handling of the scroll events and updating of the view properties accordingly now take place in the . native realm We’re still not happy though. This implementation may be performant, but it’s rather complex and requires native expertise. Can we do the same from JavaScript? Third time’s a charm — declarative API for the win So, can we do the same from JavaScript? This is the million dollar question for React Native. For the framework to be truly useful, we must find ways to resolve these performance issues without resorting to native code. This is one area where a lot of progress has been made since the . The key to reducing passes over the bridge is . It allows us to declare behaviors in advance in JavaScript and serialize the entire declaration and send it once over the bridge during initialization. From this point on, a general purpose — one that you don’t need to write yourself — will execute the behavior in the according to the declared specification. previous post declarative API native driver native realm The behavior we want consists of two parts — listening to scroll position changes and updating view properties (opacity and scale). For the latter, we know that , the excellent animation library that’s part of the core, provides good support. Animated What is less known is that Animated can drive an based on as well. [Animated.Value](https://facebook.github.io/react-native/docs/animated.html#animatedvalue) scroll events How does that work? Let’s jump into the complete implementation: We’ve started by defining an that we’ll use to hold the scroll position at any given time. We’re providing our ListView with a custom ScrollView component — an instead of a regular ScrollView. The lets us declare that our is driven by the property of the native event. Animated.Value Animated.ScrollView Animated.ScrollView Animated.Value contentOffset onScroll From this point forward, we can use the standard Animated approach of interpolating view properties based on an . This requires changing our header image to and allows us to interpolate both the and from scroll position. Animated.Value Animated.Image opacity transform.scale Note that the entire implementation is . We no longer have an imperative function that performs calculations for our effects. declarative onScroll Also note that we’ve specified . The implementation of the Animated library in recent versions of React Native finally contains a that can execute the entire declaration from the without using the bridge. useNativeDriver native driver native realm Third attempt — performance analysis As usual, we’ll count passes by analyzing what’s running in the (purple on the left) and what’s running in the (black on the right): JavaScript realm native realm The only part running in the is the initialization of our declaration. The entire declared behavior is serialized and sent over the bridge once in order to configure the general-purpose of Animated. JavaScript realm native driver After the driver has been configured, it takes care of the frame by frame for us in the without additional passes over the bridge. This section has been grayed out since it’s no longer under the responsibility of our code. native realm Comparing the three implementations The full code of all three implementations is available on GitHub: _rn-perf-experiments2 - React Native performance experiments revisited_github.com wix/rn-perf-experiments2 The demo project lets you choose which implementation to use so they can be compared side by side. Try to run it on a real device. Judging performance on a simulator is usually inaccurate. Since it’s just a simple demo app, there is very little unrelated bridge activity. You will probably not notice any difference between implementations. To assist with this, the demo app provides a toggle to simulate stress conditions with plenty of activity. As you can see, turning it on will make the performance differences much more evident: Summary Architecting performant React Native app is not always straightforward. You should normally start with the naive approach, but if you begin to notice performance issues with your app, always consider the number of passes over the bridge. As the framework matures, common tasks that are prone to high bridge traffic are addressed with declarative API that is designed to reduce passes over the bridge. The library is a good example. Another interesting example is which I’ve demonstrated in . To learn more about it, check out the post “ ”. Animated react-native-interactable ReactConf 2017 Moving Beyond Animations to User Interactions at 60 FPS in React Native If you hit a wall and can’t find a declarative API that resolves your performance issue, you can always fall back on a native implementation. Bring a native developer or two into your team and port the problematic area to native like we did in the second implementation. It’s not always pretty, but it works.