Ah, React Native animations. Long-considered to be one of the pain points of React Native development, due to their steep learning curve and being packed with potentially performance-crushing pitfalls, creating delightful animations and gesture-based interactions has been hard. But why so tricky? The answer lies in React Native’s architecture, namely with something called the bridge. In order to understand why, let’s have a brief look at React Native’s architecture to find out more.
In order to provide close-to-native performance, React Native uses a dual-thread architecture – a JS thread, which runs your React Native code, and a native UI thread. Your React Native components are effectively specifications for native UI components – layout is taken care of by a Flexbox engine – you in theory get Native UI performance (in reality, your mileage may vary) but get to use your favourite UI framework, React, with some caveats. These two threads communicate via a glorified pipe with some extra features known as the bridge. This bridge is fully asynchronous, and lets one call to native code from JS, also asynchronously. The official React Native docs explain in more detail.
The bridge is often the performance bottleneck in React Native projects, as communication is orders of magnitude slower than most other operations (especially on Android). This isn’t because it’s poorly written, it’s just the nature of the beast. In any case, minimising over-the-bridge communication is a good general strategy for achieving native-like performance in any app.
Animations and the Bridge
Looking at this architecture, animations stick out as something very difficult to do performantly. How are we supposed to achieve native performance if we need to, for example, send an object’s new position over the bridge once a frame, 60 times a second? Thankfully, the engineers working on React Native thought of that. What if we could use the native animation engine on both platforms? That way the animation gets done natively, no bridge comms needed. So that’s what we can do in React Native – specify an animation in JS that gets executed on the UI thread. This approach is possible with the Animated API that ships with React Native, but again we’re hit with some limitations. Some style properties can’t be animated using the UI thread, so we’ve got to fall back to the JS thread for those. And, as expected, performance is not great (specifically on lower-end Android devices), and I’m being diplomatic.
React Native Reanimated
In a bid to solve this issue, a group of engineers in the community decided the Animated API didn’t offer real low-level access to animations, and no granular way to express animations and no easy way to create gesture-based animations, so enter
react-native-reanimated. Reanimated solves all these problems, providing the promised low-level abstraction over native animations.
But something still wasn’t right. Reanimated v1 suffers from a very steep learning curve, as complex native animations using the library have to be expressed with a pure functional node-based style, which is not in the wheelhouse of most developers. For instance, here is an excerpt of an animation to make a circle grow and shrink repeatedly:
Not the easiest thing to get one’s head around.
Still, the community came to the aid of struggling developers, with excellent video series showing the power of Reanimated by awesome people such as William Candillon demystifying things for everyone, and recreating complex interactions in popular native apps in React Native with Reanimated. Even with this, the conclusion for me and many other React Native developers was this: you could do all the delightful stuff you see in native apps, but it was too complicated to learn. Any solution developed by the community, no matter how accomplished, kept running up against the limits React Native’s architecture, thus became more and more complex and more difficult to learn. Thankfully help was just around the corner.
For the last while, the Facebook team have been hard at work on a re-architecture of React Native, to solve some of the problems we’ve seen so far. It’s complicated, and if you’re looking for a more in-depth look at the re-architecture this series is excellent.
Anyway, the short version is: You don’t need to cross the bridge to talk to the native side anymore. What this means for us is that you can now share animation (or indeed any) values between native code and the React Native thread, without having to cross the bridge. Hooray! These new native modules that can be interacted with without crossing the bridge are called TurboModules.
Version 2 of React Native Reanimated is on the way, currently in alpha — and it's amazing. It’s completely different to v1. The latest alpha is even supported by the managed workflow in Expo SDK 39 🔥
By using this new architecture, all of the headaches caused by the bridge are removed; we no longer need to specify our animation purely functionally as nothing needs to get serialized and sent over the bridge before the animation begins to enjoy native level performance.
We can now see and interact with the animation values on the UI thread, meaning we can now inspect as well as drive the animation imperatively from the JS thread!
Reanimated 2 introduces the concept of a worklet. This is a JS function that can run in a tiny JS context on the UI thread. It’s dead easy to turn a regular JS function into a worklet using a directive:
'worklet'; directive basically just means that this function can be executed on the UI thread.
Because of the lack of a bridge, animated values are now referred to as shared values, because they are quite literally shared with the UI thread.
This allows us to modify them directly and update an animation’s progress from the JS thread simply by doing the following:
useSharedValue is a convenient React Hook shipped with Reanimated 2 to create a shared value.
useAnimatedStyle is another helper from Reanimated which ensures our component re-renders when the animated value updates.
This component moves 100 pixels to the right after one second after mounting.
Piece of cake 🍰
If we wanted this transition to animate smoothly, the team behind Reanimated thought of that. We can wrap our assignment in an animation helper to animate the transition:
Now the value will smoothly transition to the target value
100 over the course of 1000ms. Neat!
Combining what we learned, let’s see how the circle animation before looks with the new API:
How easy was that?
That’s not all — it’s now just as easy to respond to gestures and other input events, then trigger & update animations in the JS thread imperatively and get native animation performance, as shown in this video. As before, you can do the delightful stuff you see in native apps. But this time it’s easy.
It’s extremely exciting when something like this happens. React Native is now more than 5 years old and it’s so great to see improvements as fundamental and value-adding as this after as much time. Amazing work and support is still being devoted to the project by an accomplished and helpful community. Thanks guys!
tl;dr — See title.
Thanks for reading!