We can’t directly pass Dictionary to Native, because yogo will parse these large amounts of data, which will cause performance problems. We’d better serialize them into JSON strings and pass large amounts of data between JS and Native. We calculate all index values in the background thread and then update the UI on the main thread
There are many kinds of charts and indicators that need to be drawn, so we encapsulate the same parts of them into protocols. Similarly, in swift, we can add default implementations to protocols, or add public methods
We need a common point conversion method. We first draw the candles of the main chart and the sub-chart, and then draw the text, ruler, maximum and minimum, time scale, current price horizontal line, long-pressed detail panel, etc.
When the ScrollView scrolls horizontally, in order to save performance, we only draw in the visible range of the screen, and then calculate the accurate offset for drawing
Our finger mark drawings are all drawn through HTDrawContext, we need to pay attention to including parallelograms, we need to convert all coordinates to the current price coordinates, because our zoom gesture needs to keep these finger mark drawings in the correct position
We can know that the best animation performance of ReactNative is LayoutAnimation UIView animation, The best gesture performance is to use NativeEvent. These two solutions point to one direction, that is, to solve the performance problem of ReactNative. We can’t let the busy JS thread participate in these calls, so in terms of performance, I need to change from color to gradient The sliding and size of the text scaling and the scroll bar are all driven by the onScroll nativeEvent event, which is completely separated from the JS call to achieve the best performance
How to realize Label’s color gradient and text size scaling according to onScroll
We first bind the transform of the Label to the onScroll event. When Native needs to change the transform according to the onScroll calculation, we take the opportunity to change the gradient color of the Native
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
//HTPageHeaderView _itemTitleProps = (item, index) => { let fontScale = 1 + (this.itemTitleSelectedStyle.fontSize - this.itemTitleNormalStyle.fontSize) / this.itemTitleNormalStyle.fontSize if (fontScale == 1) { fontScale = 1.0001 } let scale = this.scrollPageIndexValue.interpolate({ inputRange: [index - 2, index - 1, index, index + 1, index + 2], outputRange: [1, 1, fontScale, 1, 1] }) let titleStyle = { ... style: { ...this?.props?.itemTitleStyle, transform: [{ scale }] }, } return titleStyle }
We not only need to change the gradient color of the text, because the size of the text has also been changed, so we need to reset the size of the component through setIntrinsicContentSize for other components to re-typesetting
How to change the position and length of Cursor according to onScroll
If there is a fixed cursor length, then we can calculate the x offset that the cursor should have for each index If there is no fixed cursor length, then we take the length of Label as the standard
Where is the performance bottleneck of react-navigation
We can know that the best animation performance of ReactNative is LayoutAnimation UIView animation, The best gesture performance is to use NativeEvent. These two solutions point to one direction, that is, to solve the performance problem of ReactNative. We cannot let busy JS threads participate in these calls. However, react-navigation from onPress to navigation.push There are JS calls, so our task today is to customize a navigation architecture to greatly improve the performance of our ReactNative application
Encapsulate NavigationController
Starting from the animation, we need to use the UIView process animation that comes with Native. Then the iOS platform already has a UINavigationController library. We only need to create a UINavigationController on the Android platform.
We need to handle the hiding of the TabBar
We need to hide the Controller that has been covered by the top page to save memory
And we need to handle viewDidAppear and viewDidDisappear lifecycle
// NavigationController publicvoidpopToViewController(final HTRouteController controller, Boolean animated){ if (childControllerList. size() <= 1) { return; } if (this. lock) { return; } this. lock = true; finalint index = childControllerList. indexOf(controller); final HTRouteController animatedController = childControllerList. get(childControllerList. size() - 1); if (index == 0) { HTRouteTabBarController tabBarController = HTRouteGlobal.nextController(getView(), HTRouteTabBarController.class); if (tabBarController != null) { tabBarController. reloadShowTabBar(true); } } controller.getView().setVisibility(View.VISIBLE); translateAnimation(animatedController, animatedController.getView(), false, animated, new Callback() { @Override publicvoidinvoke(Object... args){ controller. viewDidAppear(); animatedController. viewDidDisappear(); int size = childControllerList. size(); List<HTRouteController> willRemoveControllerList = new ArrayList<>(); for (int i = index + 1; i < size; i ++) { HTRouteController removeController = childControllerList. get(i); willRemoveControllerList. add(removeController); } for (HTRouteController removeController: willRemoveControllerList) { removeChildController(removeController); } lock = false; } }); }
Package TabBar
From gesture analysis, we need to avoid JS.onPress, so we use the package of UITabBarController and UITabBar on the iOS platform. We only need to create a UITabBar and UITabBarController on the Android platform.
Due to the diversity of TabBar, we just encapsulate a common example, and other projects can inherit and rewrite
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
int bottom = 0; if (!TextUtils.isEmpty(HTRouteGlobal.getProp("ro.build.version.emui"))) { bottom = 1; } layoutParams.setMargins(0, 0, 0, bottom); fragmentContainer.addView(view, layoutParams);
fragment. viewDidAppear();
}
Encapsulate RouteView
The most important point is that we can bind the pages and parameters that need to be redirected to RouteView. When the original click event is received, we can directly jump to the native page without going through JS.
We need to leave the same interface as react-navigation on the JS side to facilitate switching at any time
JS parameter passing
If function parameters need to be passed between pages, we need to store these variables in global variables, and then retrieve them through the key on the second page, because functions cannot be serialized
const decodeRouteData = (props) => { let reloadProps = { ... props } let componentRouteOptionList = reloadProps?. componentRouteOptionList ?? {} let id = componentRouteOptionList?.id sureComponentItem(id)
let componentValueList = globalValue. componentList[id] ?? {} let componentPropList = componentValueList['componentPropList'] ?? {} reloadProps.componentPropList = componentPropList return reloadProps }
JS Lifecycle
We need to receive the viewDidAppear and viewDidDisappear life cycle events sent by the native side, and then call the method of the corresponding component through the id
let componentFunction = componentRef[actionName] if (componentFunction == null) { return } componentFunction. call(componentRef, value) })
Affiliated
The full code is here https://github.com/hellohublot/react-native-route 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
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), )
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]; }
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?
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