Skip to content
This repository was archived by the owner on Nov 27, 2022. It is now read-only.

Commit 346f22d

Browse files
committed
fix: memoize animated nodes to improve performance
Currently the animated nodes used for styles are re-created in render, which means that reanimated does a lot of expensive work detaching and reattaching them. Due to this, the framerate of the JS thread goes down to 0 FPS even on high-end devices when switching tabs quickly. This PR implements memoization for the animated nodes so they are only recreated when necessary. On low-end devices, this keeps the JS frame rate around 12+ FPS on low-end devices and 40+ on high-end devices. Related to #720
1 parent aa7e9b3 commit 346f22d

File tree

5 files changed

+284
-199
lines changed

5 files changed

+284
-199
lines changed

src/Pager.js

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as React from 'react';
44
import { StyleSheet, Keyboard, I18nManager } from 'react-native';
55
import { PanGestureHandler, State } from 'react-native-gesture-handler';
66
import Animated, { Easing } from 'react-native-reanimated';
7+
import memoize from './memoize';
78

89
import type {
910
Layout,
@@ -568,6 +569,25 @@ export default class Pager<T: Route> extends React.Component<Props<T>> {
568569
this._progress,
569570
]);
570571

572+
_getTranslateX = memoize(
573+
(
574+
layoutWidth: number,
575+
routesLength: number,
576+
translateX: Animated.Node<number>
577+
) =>
578+
multiply(
579+
// Make sure that the translation doesn't exceed the bounds to prevent overscrolling
580+
min(
581+
max(
582+
multiply(layoutWidth, sub(routesLength, 1), DIRECTION_RIGHT),
583+
translateX
584+
),
585+
0
586+
),
587+
I18nManager.isRTL ? -1 : 1
588+
)
589+
);
590+
571591
render() {
572592
const {
573593
layout,
@@ -577,17 +597,10 @@ export default class Pager<T: Route> extends React.Component<Props<T>> {
577597
removeClippedSubviews,
578598
} = this.props;
579599

580-
// Make sure that the translation doesn't exceed the bounds to prevent overscrolling
581-
const translateX = min(
582-
max(
583-
multiply(
584-
this._layoutWidth,
585-
sub(this._routesLength, 1),
586-
DIRECTION_RIGHT
587-
),
588-
this._translateX
589-
),
590-
0
600+
const translateX = this._getTranslateX(
601+
this._layoutWidth,
602+
this._routesLength,
603+
this._translateX
591604
);
592605

593606
return children({
@@ -610,13 +623,7 @@ export default class Pager<T: Route> extends React.Component<Props<T>> {
610623
layout.width
611624
? {
612625
width: layout.width * navigationState.routes.length,
613-
transform: [
614-
{
615-
translateX: I18nManager.isRTL
616-
? multiply(translateX, -1)
617-
: translateX,
618-
},
619-
],
626+
transform: [{ translateX }],
620627
}
621628
: null,
622629
]}

src/TabBar.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
ViewStyleProp,
1212
TextStyleProp,
1313
} from 'react-native/Libraries/StyleSheet/StyleSheet';
14+
15+
import memoize from './memoize';
1416
import type {
1517
Route,
1618
Scene,
@@ -53,7 +55,6 @@ export type Props<T> = {|
5355
|};
5456

5557
type State = {|
56-
scrollAmount: Animated.Value,
5758
initialOffset: ?{| x: number, y: number |},
5859
|};
5960

@@ -90,7 +91,6 @@ export default class TabBar<T: Route> extends React.Component<Props<T>, State> {
9091
: undefined;
9192

9293
this.state = {
93-
scrollAmount: new Animated.Value(0),
9494
initialOffset,
9595
};
9696
}
@@ -109,6 +109,8 @@ export default class TabBar<T: Route> extends React.Component<Props<T>, State> {
109109
}
110110
}
111111

112+
_scrollAmount = new Animated.Value(0);
113+
112114
_scrollView: ?ScrollView;
113115
_isManualScroll: boolean = false;
114116
_isMomentumScroll: boolean = false;
@@ -209,6 +211,10 @@ export default class TabBar<T: Route> extends React.Component<Props<T>, State> {
209211
this._isManualScroll = false;
210212
};
211213

214+
_getTranslateX = memoize((scrollAmount: Animated.Node<number>) =>
215+
Animated.multiply(scrollAmount, -1)
216+
);
217+
212218
render() {
213219
const {
214220
position,
@@ -239,7 +245,7 @@ export default class TabBar<T: Route> extends React.Component<Props<T>, State> {
239245
const { routes } = navigationState;
240246
const tabWidth = this._getTabWidth(this.props);
241247
const tabBarWidth = tabWidth * routes.length;
242-
const translateX = Animated.multiply(this.state.scrollAmount, -1);
248+
const translateX = this._getTranslateX(this._scrollAmount);
243249

244250
return (
245251
<Animated.View style={[styles.tabBar, style]}>
@@ -282,7 +288,7 @@ export default class TabBar<T: Route> extends React.Component<Props<T>, State> {
282288
[
283289
{
284290
nativeEvent: {
285-
contentOffset: { x: this.state.scrollAmount },
291+
contentOffset: { x: this._scrollAmount },
286292
},
287293
},
288294
],

src/TabBarIndicator.js

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import * as React from 'react';
44
import { StyleSheet, I18nManager } from 'react-native';
55
import Animated from 'react-native-reanimated';
66
import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';
7+
8+
import memoize from './memoize';
79
import type { Route, SceneRendererProps, NavigationState } from './types';
810

911
export type Props<T> = {|
@@ -15,29 +17,38 @@ export type Props<T> = {|
1517

1618
const { max, min, multiply } = Animated;
1719

18-
export default function TabBarIndicator<T: Route>(props: Props<T>) {
19-
const { width, position, navigationState, style } = props;
20-
const { routes } = navigationState;
21-
22-
const translateX = multiply(
23-
max(min(position, routes.length - 1), 0),
24-
width * (I18nManager.isRTL ? -1 : 1)
20+
export default class TabBarIndicator<T: Route> extends React.Component<
21+
Props<T>
22+
> {
23+
_getTranslateX = memoize(
24+
(position: Animated.Node<number>, routes: Route[], width: number) =>
25+
multiply(
26+
max(min(position, routes.length - 1), 0),
27+
width * (I18nManager.isRTL ? -1 : 1)
28+
)
2529
);
2630

27-
return (
28-
<Animated.View
29-
style={[
30-
styles.indicator,
31-
{ width: `${100 / routes.length}%` },
32-
// If layout is not available, use `left` property for positioning the indicator
33-
// This avoids rendering delay until we are able to calculate translateX
34-
width
35-
? { transform: [{ translateX }] }
36-
: { left: `${(100 / routes.length) * navigationState.index}%` },
37-
style,
38-
]}
39-
/>
40-
);
31+
render() {
32+
const { width, position, navigationState, style } = this.props;
33+
const { routes } = navigationState;
34+
35+
const translateX = this._getTranslateX(position, routes, width);
36+
37+
return (
38+
<Animated.View
39+
style={[
40+
styles.indicator,
41+
{ width: `${100 / routes.length}%` },
42+
// If layout is not available, use `left` property for positioning the indicator
43+
// This avoids rendering delay until we are able to calculate translateX
44+
width
45+
? { transform: [{ translateX }] }
46+
: { left: `${(100 / routes.length) * navigationState.index}%` },
47+
style,
48+
]}
49+
/>
50+
);
51+
}
4152
}
4253

4354
const styles = StyleSheet.create({

0 commit comments

Comments
 (0)