Hello, everyone 👋
Reanimated 2 came up with a new way of building animations in react-native that runs at 60FPS. So just like you, I was hell curious to know the secret behind all of the magic reanimated does. And after my learnings about Reanimated internals, I thought to write a blog about the same and share my understandings with you.
Note: All contents of this blog are from my understanding and how I perceived all of the knowledge that means I can be wrong in many places and I would be really happy to get corrected.
Before talking about the internals of reanimated 2 I would like to talk about how our UI is handled in an app, how animations are currently done in react-native, and its problems.
How does a traditional app handle its UI?
When an application is launched, the system creates a thread of execution for the application, called main thread. This thread is very important because it is in charge of dispatching events to the appropriate user interface widgets, including drawing events.
Also, the main thread is also sometimes called the UI thread. However, under special circumstances, an app's main thread might not be its UI thread.
Source and more elaborate version on this available here
Going further we are going to use the term UI thread more because it sounds easier to understand with the context of UI.
As you can see in the above image that we got a set of UI components drawn in mobile, now to update any property of components like changing button text we need to update it from the UI thread because that's where they live.
Role of UI thread in the context of react-native
Whenever React-Native apps boot up, it will spawn a thread to handle our javascript code. This thread contains a javascript virtual machine to run our javascript code and it's the same place where our fancy react lives. we will refer to this thread as JS thread.
Now as we talked about earlier that whenever we need to do anything about the component UI or its property, we need to do it from the UI thread. So our JS thread sends JSON messages to UI thread, and does the necessary changes on UI.
Communication between UI thread and JS thread is done by a bridge and it's totally asynchronous in nature.
Refer to the above image as a mental model of how UI changes are done in React-Native.
Current state of animations in react-native
Whenever we want to animate anything in react-native, we need to send messages to UI thread so they can animate our views. With useNativeDriver set to true, react-native just initiates the animation from the JS thread to UI, and all animation calculation including updating of animation values is done on the native side. This unblocks our JS thread to perform other critical tasks.
if required useNativeDriver is set to false, react-native calculates everything on the JS thread and sends multiple messages to native to update the animation values and UI.
Please look at the attached images for a better understanding.
This way of doing animations is great but gets its own set of tradeoffs. for example, if our thread is blocked with many messages then our animation won't initiate on time causing a lag, and animations are a crucial part of user interactions and it should be silky smooth. Reanimated 2 addresses these issues and helps us to create silky-smooth animations.
Reanimated 2
So to understand the internals of Reanimated 2 I started digging the source code of it and their fundamental section on docs also clears many concepts. I asked a lot of questions to Marc on discord and Twitter about JSI, Reanimated and I am really grateful to him for taking out his time and answering my questions.
Reanimated spawns a thread that lives on the UI thread. This thread contains small pieces of javascript code and with the help of JSI they can directly synchronously communicate with the UI thread for animating the UI. Communication between the REA2 thread and main JS thread is also done with JSI
I know you might be thinking what's the benefit of spawning an additional thread?. While making complex animation there are many calculations and gestures to handle and as we know that the main JS thread is already very busy executing business logic and there are cases where our animations are delayed and we would never want that. so by spawning a different thread we can move our animation and gesture-driven logic to a separate place where they can run uninterruptedly from the main JS thread.
You can refer to the above image as a mental model for the REA2 thread.
You might be thinking about what's the purpose of JSI is? Using JSI, JavaScript can hold a reference to C++ Host Objects and invoke native(java/ objc) methods from them, this makes the communication between native and javascript synchronous.
Follow this proposal for more elaborate info
Worklets
Worklets play an important role in reanimated. Functions marked with “worklet” directive are picked by reanimated babel plugin and then these functions along with the variables used inside them are captured and copied in the javascript runtime existing in reanimated 2 thread. The copied variables and functions in the REA2 JS Runtime are frozen which means they are Immutable in reanimated 2 thread.
Executing a worklet marked function on the main JS thread will work just like a normal function.
An example of how our code looks in both threads, refer to the below-attached image as the main code.
Below attached images show how the main code looks like in both the threads, note it's not the exact representation but you can have an idea about what's happening.
useSharedValue
useSharedValue are mutable values between main JS thread and REA2 thread, and they are reactive for driving animation in reanimated 2. Whenever we use useSharedValue it will create that value on the main JS thread, and CPP(from JSI) will hold the reference of the created value and make a copy from that reference on the REA2 JS Runtime.
So whenever we change the value of useSharedValue from one thread, the other thread can receive the new value because they are interconnected via CPP reference.
Now as useSharedValue is getting shared in 2 threads, there can be concurrency issues so to prevent them reanimated does a neat trick whenever we try to manipulate useSharedValue from REA2 runtime the changes would be immediate and synchronous but when we try to manipulate useSharedValue from main JS thread, it will be asynchronous just like state update. This makes a lot of sense because most of the animation calculations with useSharedValue exists on the REA2 runtime.
You can refer the above image for a mental model of useSharedValue and it's not exactly what happening behind the scene
runOnJS
When executing in REA2 JS Runtime there could be a need to execute a function call that exists on the main JS thread.
For example, after this gesture ends, update a certain state. Now state updates can only be done from the main JS thread. Thanks to runOnJS we can enqueue a function call on the main JS thread from our REA2 Runtime. The function call won't be immediate because the main JS thread could be busy handling something else.
You can refer to the above-attached image for a mental model of runOnJS
runOnUI
runOnUI is very similar to runOnJS, So as we talked earlier about worklets and how it moves pieces of code into REA2 runtime. So for every worklet marked function we can use runOnUI to execute that function on REA2 JS Runtime, behind the scenes with the help of JSI we enqueue that function on REA2 JS runtime, and the function call won't be immediate because REA2 JS runtime could be busy handling something else.
You can refer the above-attached image for a mental model of runOnUI
The end
Thank you all for sticking to the end and giving me a chance to share my learning and understanding of the internals of reanimated 2. I know I can be wrong in many places please feel free to correct me.