0%

Analyzing and optimizing ReactNative animations and gestures through source code

Why is it stuck


Although the Main thread of most ReactNative applications does keep 60FPS, why sometimes we still feel stuck,
In fact, this is because of the asynchronous communication between the JS thread and the Main thread, resulting in the user’s gestures not being feedback in time, so our article mainly talks about the performance optimization of ReactNative applications


Animation

JS frame animation

1
2
3
4
5
6
7
8
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/Libraries/Animated/animations/TimingAnimation.js#L126
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/Libraries/Core/Timers/JSTimers.js#L257
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/React/CoreModules/RCTTiming.mm#L334
// TimingAnimation
// Through the above call chain, you can see that the JS frame animation is triggered based on the timer loop
this._animationFrame = requestAnimationFrame(
this.onUpdate.bind(this),
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/React/CoreModules/RCTTiming.mm#L296
// RCTTiming
// Start an NSTimer on the JS thread
- (void)scheduleSleepTimer:(NSDate *)sleepTarget
{
@synchronized(self) {
if (!_sleepTimer || !_sleepTimer.valid) {
_sleepTimer = [[NSTimer alloc] initWithFireDate:sleepTarget
interval: 0
target:[_RCTTimingProxy proxyWithTarget:self]
selector: @selector(timerDidFire)
userInfo: nil
repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:_sleepTimer forMode:NSDefaultRunLoopMode];
} else {
_sleepTimer.fireDate = [_sleepTimer.fireDate earlierDate:sleepTarget];
}
}
}

requestAnimationFrame is a timer function exposed by Native to JS. Every time the timer triggers a frame animation, the JS thread will recalculate the layout, and then switch to the Main thread to update these layouts. Ordinary JS animations are generally very stuck, because the frame timer is in the The thread may be very busy, the communication between threads may take time to wait, and the frame timer keeps making the main thread refresh the interface while the main thread may also be busy, so it is very common to encounter performance problems when using JS animation. What are the common solutions?

Native frame animation useNativeDriver

1
2
3
4
5
6
7
8
9
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m#L429
// RCTNativeAnimatedNodesManager
// Start a CADisplayLink on the main thread
- (void)startAnimationLoopIfNeeded {
if (!_displayLink && _activeAnimations.count > 0) {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(stepAnimations:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m#L85
// RCTFrameAnimation
// The selector bound to CADisplayLink will calculate the value change, and then directly call setNeedsDisplay on the main thread
- (void)stepAnimationWithTime:(NSTimeInterval)currentTime
{
...
[self updateOutputWithFrameOutput:frameOutput];
}

- (void)updateOutputWithFrameOutput:(CGFloat)frameOutput
{
...
[_valueNode setNeedsUpdate];
}

useNativeDriver, what is the actual function of this property? Looking the source code of ReactNative or react-native-reanimated, we can see that a variable value is bound in the native side, and a CADisplayLink is created. This frame timer is to change the bound variable value frame by frame, and then refresh the corresponding interface after the value is changed in the main thread, which will not involve JS thread communication, so most of the cases can help solve our performance problems, but there are sometimes we will still encounter animation performance problems that cannot be solved by useNativeDriver, Because the frame loss of CADisplayLink frame animation, how do we deal with this situation?

Native layout animation LayoutAnimation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/React/Modules/RCTLayoutAnimation.m#L107-L127
// RCTLayoutAnimation
// LayoutAnimation will perform UIView animation through UIManager
- (void)performAnimations:(void (^)(void))animations withCompletionBlock:(void (^)(BOOL completed))completionBlock
{
if (_animationType == RCTAnimationTypeSpring) {
[UIView animateWithDuration:_duration
delay:_delay
usingSpringWithDamping:_springDamping
initialSpringVelocity:_initialVelocity
options:UIViewAnimationOptionBeginFromCurrentState
animations: animations
completion:completionBlock];
} else {
UIViewAnimationOptions options =
UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionsFromRCTAnimationType(_animationType);

[UIView animateWithDuration:_duration
delay:_delay
options:options
animations: animations
completion:completionBlock];
}
}

LayoutAnimation can solve this problem, and why, we can find out by looking at the source code, because LayoutAnimation does not start the native frame animation, but the native UIView animation that comes with iOS, because the animation of iOS is a separate process for animation. This animation is a process animation and has nothing to do with threads, so even if the Main thread is too busy, the animation process will directly complete the animation. Similarly, the animation framework of android may also have its own optimization, such as hardware acceleration, etc.

Gesture Events

JS Gesture Events

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/React/Base/RCTTouchHandler.m#L186
// RCTTouchHandler
// Every touch event on RootContentView will be sent to JS
- (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches eventName:(NSString *)eventName {
...
RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
reactTag: self.view.reactTag
reactTouches: reactTouches
changedIndexes: changedIndexes
coalescingKey:_coalescingKey];

if (!canBeCoalesced) {
_coalescingKey++;
}

[_eventDispatcher sendEvent:event];
}

Ordinary JS gesture processing is generally very slow, because the main thread receives the gesture, sends the gesture to the JS thread, and then executes the JS method. The Main thread receives the interface properties that should be updated, which involves two cross-threads Transferring data, among them, it is absolutely possible to wait for communication. How do we usually solve it?

Native Gesture Events

Similarly, the useNativeDriver mentioned above, we also have an event called nativeEvent, usually, them are used together, the native gesture will send a global notification, the animation framework will listen to this event, and then directly update the value of the animation binding As well as refreshing the interface directly on the main thread, we also don’t need asynchronous JS thread communication, so react-native-gesture-handler really solves a lot of performance problems

1
2
3
4
5
6
// https://github.com/facebook/react-native/blob/26b2bb5343f92672ed4e8f42c6f839e903124b06/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm#L76
// RCTNativeAnimatedModule
// The animation framework will listen to all Event events, and then judge whether to update the bound animation value value according to the key
- (void)initialize {
[[self.moduleRegistry moduleForName:"EventDispatcher"] addDispatchObserver:self];
}

Affiliated

I’m hublot, sharing some of my thoughts, I’m too busy with work, it’s inevitable that there will be negligence, please forgive me