- This story is my experience and experiments to achieve high performance animation in React Native.Along the way I learned tons from these blogs and recommend you read them as you dive deeper into building your own animations in React Native:React Native Animations Using Animated API from Nader DabitReact Native ART and D3 from Harry WolffBefore we continue, I would like to define what “high performance animation” in terms of my project.
- Note: There is another OpenGL binding for React Native which enables a more flexible OpenGL implementation without the interface.Animation in React NativeIn my exploration I tested three major methods (with some variations) for doing shape animation with React Native.
- The gaps between each country will also get scaled so the transformed shapes are not the same as the new shapes.Zoom animation with fade in/outThis method has a significant impact on the memory footprint since there are two views with a shadow node per shape.Animated + Hybrid Native Driver + Animated Shape (ART)For this approach I still used the Native Driver while I found there is a trick to communicate between the JS thread and UI thread using setNativeProps.
- I first tried with React Native ART and switched to React Native SVG while both give similar results.Animated + Hybrid Native Driver + Animated Shape (OpenGL)After realizing the bottleneck comes from re-drawing shapes, I switched to another library which has the drawing logic implemented in OpenGL.
Archiving decent animation performance is quite tricky. This story is my experience and experiments to achieve high performance animation in React Native.
@salesforcejobs: Three major methods for shape animation with React Native via @salesforceeng
Along the way I learned tons from these blogs and recommend you read them as you dive deeper into building your own animations in React Native:
Before we continue, I would like to define what “high performance animation” in terms of my project. The goal it to animate a world map of polygons to another set of polygons, like:
All the polygons are drawn frame by frame. Each polygon will have around 100 vertices and there are hundreds of polygons. We will need to animate around 10000 vertices every frame, including native-side data exchange, interpolation, and rendering.
The first step is to draw shapes. Here are few ways (libraries) I’ve tried:
that can be used to construct any shape. On the native side, React Native ART translates paths to Core Graphics on iOS and Canvas on Android. This library can do almost any shape while the performance is bound with native graphics performance which degrades with many shapes and frequent updates. Memory-wise, since only the root view is constructed with the shapes having only shadow nodes, it is more efficient than pseudo shapes. I found this documentation useful.
React Native SVG — This was branched from React Native ART and provides an HTML SVG-like interface. As result, the backend uses Core Graphics and Canvas. I have read that this library is somewhat less performant than React Native ART, but I found similar performance between them. The difference is more about the API side (declarative API for SVG) than the performance.
In my exploration I tested three major methods (with some variations) for doing shape animation with React Native. Here are those methods and the pros & cons of each:
Layout Animation — Another alternative is the layout animation. It has a very easy way to setup the animation by simply adding one line of code before you change the properties which will trigger a layout change. The whole animation is nicely implemented in the native side which has great performance. The big downside of this approach is it only supports layout properties (left, top, flex… but no width/height). For my use case, this is far from enough — even wrapping all the shapes inside views.
Animated API — The Animated API is a set of declarative animation APIs. The big idea of React is to only update child elements when data changes. However, this conflicts with the animation use case. Most of the time when doing animation, we only want to update the properties of some node without modifying its children. To avoid the unnecessary updating, the Animated API uses animation variables that nodes can bind to. The main difference is we do not have to worry about implementing tweening (interpolation) logic ourselves. However, this API did not help with the performance until early this year when they introduced the concept of the native driver.
Animated API + Native Driver — This is a new mechanism introduced early this year to alleviate the problems mentioned above. Instead of communicating between the JS thread and the native thread every frame, it sends off the entire animation information, all at once, before the animation starts. With this approach, we save not only the serialization/deserialization time, but also improve the performance by doing all the interpolation in the native thread. It is expectedly more performant than the JS Driver. However, at the time of writing, the Native Driver only supports non-layout properties — transforms (translate, rotate, scale), color, and opacity. For example, if we have a shape with a 1px border, it cannot be transformed to have a larger width/height and retain the same border width without redrawing.
for this approach, but it might help the performance to some extent.
to the views. The following numbers were collected from an iPhone 6 Plus and Samsung S2:
The performance is impressive with the Native Driver. However, due to the limitation of being able to use only transforms, I can only achieve the animation below (which fades out the scaled shapes.) The gaps between each country will also get scaled so the transformed shapes are not the same as the new shapes.
This method has a significant impact on the memory footprint since there are two views with a shadow node per shape.
to update the native module. With this hybrid approach, even though we cannot take the full advantage of the Native Driver, we can morph the shape anyway we want. The following is the performance collected from the same devices:
We can clearly see the performance drops significantly since for each frame all the shapes redraw. I further profiled this setup with 500 shapes:
The first row is the JS thread which handles the serialization and the second row marked as main is the Native UI thread. The grey and white colors mark the interval for 16ms per frame. This indicates the bottleneck is Core Graphics rendering 500 shapes, not the communication payload from the JS thread to UI thread. I first tried with React Native ART and switched to React Native SVG while both give similar results.
so I had to add some glue code. The library does a good job queue consecutive render calls. As a result, the it performs much better than Core Graphics in terms of drawing hundreds of shapes per frame.
It works great! I did not measure the iOS here since it usually is more performant than Android. We achieved decent FPS on Android and I did not see a memory crisis here either. The only downside is we still need to communicate between the JS thread and the UI thread each frame. When the payload gets large, the serialization/deserialization time becomes the bottleneck and frames start dropping again.
This is an on-going work so I do not have the numbers yet. I have traced the React Native code and learned that even though it officially does not support properties other than transforms, it does interpolate the properties and send them to the native module as long as they have a UI view. However, we do not have a view for each shape — only shadow nodes. I think there are many ways to work around this constraint. We can potentially still fully leverage the Native Driver with limited modification in the native layer. If it works out (or in the future the React team supports it), we can have large payloads without reducing the animation performance like above.Conclusion
Several experiments prove that React Native does have the potential to support high performance animation with the right tools and some tuning. At the time of writing, the Native Driver was just released. Since this all is new, resources or documentation are scarce. I’d greatly appreciate comments and feedback so I can improve this resource over time!