Skip to content

Commit e7eaa40

Browse files
j-piaseckim-bert
andauthored
[General] Reduce the number of calls to useEvent (#3836)
## Description During testing, I've noticed that calling `useEvent` from Reanimated is relatively heavy. Since we're doing it three times, I thought it might be worth exploring how much impact it has on our hooks. This PR reduces the number of `useEvent` calls to one per gesture, which reduces the execution time of `usePan` by ~10%. I'd assume the results for other gestures will be the same, since they all depend on internal `useGesture`. ## Test plan <details> <summary>Tested on this stress-test</summary> ```jsx import React, { Profiler, useEffect, useRef } from 'react'; import { ScrollView, StyleSheet, View } from 'react-native'; import { GestureDetector, usePan } from 'react-native-gesture-handler'; // import { PerfMonitor } from 'react-native-gesture-handler/src/v3/PerfMonitor'; import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; const DATA = new Array(1).fill(null).map((_, i) => `Item ${i + 1}`); function Item() { const translateX = useSharedValue(0); const style = useAnimatedStyle(() => { return { transform: [{ translateX: translateX.value }], }; }); const pan = usePan({ // disableReanimated: true, onUpdate: (event) => { 'worklet'; console.log('pan onUpdate', event.handlerData.changeX); }, onStart: () => { 'worklet'; console.log('pan onStart'); }, onEnd: () => { 'worklet'; console.log('pan onEnd'); }, onBegin: () => { 'worklet'; console.log('pan onBegin'); }, onFinalize: () => { 'worklet'; console.log('pan onFinalize'); }, onTouchesDown: () => { 'worklet'; console.log('pan onTouchesDown'); }, }); return null; } function Benchmark() { return ( <ScrollView style={{ flex: 1 }} contentContainerStyle={{ flexGrow: 1, gap: 8 }}> {DATA.map((_, index) => ( <Item key={index} /> ))} </ScrollView> ); } const TIMES = 35; export default function EmptyExample() { const times = useRef<number[]>([]).current; const [visible, setVisible] = React.useState(true); useEffect(() => { if (!visible && times.length < TIMES) { setTimeout(() => { setVisible(true); }, 24); } if (times.length === TIMES) { // calculate average, but remove highest and lowest const sortedTimes = [...times].sort((a, b) => a - b); sortedTimes.shift(); sortedTimes.shift(); sortedTimes.pop(); sortedTimes.pop(); const avgTime = sortedTimes.reduce((sum, time) => sum + time, 0) / sortedTimes.length; console.log(`Average render time: ${avgTime} ms`); // console.log(JSON.stringify(PerfMonitor.getMeasures(), null, 2)); // PerfMonitor.clear(); } }, [visible]); return ( <View style={styles.container}> {visible && ( <Profiler id="v3" onRender={(_id, _phase, actualDuration) => { times.push(actualDuration); setTimeout(() => { setVisible(false); }, 24); }}> <Benchmark /> </Profiler> )} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, }, }); ``` </details> Before: ~280ms, after: ~253ms --------- Co-authored-by: Michał Bert <[email protected]>
1 parent 5fb9999 commit e7eaa40

File tree

11 files changed

+154
-142
lines changed

11 files changed

+154
-142
lines changed

packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ type WorkletFunction<
2424
TReturn = unknown,
2525
> = ((...args: TArgs) => TReturn) & WorkletProps;
2626

27+
export type ReanimatedHandler<THandlerData> = {
28+
doDependenciesDiffer: boolean;
29+
context: ReanimatedContext<THandlerData>;
30+
};
31+
2732
let Reanimated:
2833
| {
2934
default: {
@@ -33,10 +38,9 @@ let Reanimated:
3338
options?: unknown
3439
): ComponentClass<P>;
3540
};
36-
useHandler: <THandlerData>(handlers: GestureCallbacks<THandlerData>) => {
37-
doDependenciesDiffer: boolean;
38-
context: ReanimatedContext<THandlerData>;
39-
};
41+
useHandler: <THandlerData>(
42+
handlers: GestureCallbacks<THandlerData>
43+
) => ReanimatedHandler<THandlerData>;
4044
useEvent: <T>(
4145
callback: (event: T) => void,
4246
events: string[],

packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { useMemo } from 'react';
22
import { BaseGestureConfig } from '../../../types';
33
import { prepareStateChangeHandlers } from '../../utils';
44
import { getStateChangeHandler } from '../stateChangeHandler';
5+
import { ReanimatedContext } from 'packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper';
56

67
export function useGestureStateChangeEvent<THandlerData, TConfig>(
78
handlerTag: number,
8-
config: BaseGestureConfig<THandlerData, TConfig>
9+
config: BaseGestureConfig<THandlerData, TConfig>,
10+
context: ReanimatedContext<THandlerData>
911
) {
1012
return useMemo(() => {
1113
const handlers = prepareStateChangeHandlers({
@@ -14,12 +16,13 @@ export function useGestureStateChangeEvent<THandlerData, TConfig>(
1416
onEnd: config.onEnd,
1517
onFinalize: config.onFinalize,
1618
});
17-
return getStateChangeHandler(handlerTag, handlers);
19+
return getStateChangeHandler(handlerTag, handlers, context);
1820
}, [
1921
handlerTag,
2022
config.onBegin,
2123
config.onStart,
2224
config.onEnd,
2325
config.onFinalize,
26+
context,
2427
]);
2528
}

packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { useMemo } from 'react';
66

77
export function useGestureUpdateEvent<THandlerData, TConfig>(
88
handlerTag: number,
9-
config: BaseGestureConfig<THandlerData, TConfig>
9+
config: BaseGestureConfig<THandlerData, TConfig>,
10+
jsContext: ReanimatedContext<THandlerData>
1011
) {
1112
return useMemo(() => {
1213
const { handlers, changeEventCalculator } = prepareUpdateHandlers(
@@ -16,10 +17,6 @@ export function useGestureUpdateEvent<THandlerData, TConfig>(
1617
config.changeEventCalculator
1718
);
1819

19-
const jsContext: ReanimatedContext<THandlerData> = {
20-
lastUpdateEvent: undefined,
21-
};
22-
2320
return config.dispatchesAnimatedEvents
2421
? undefined
2522
: getUpdateHandler(
@@ -33,5 +30,6 @@ export function useGestureUpdateEvent<THandlerData, TConfig>(
3330
config.onUpdate,
3431
config.dispatchesAnimatedEvents,
3532
config.changeEventCalculator,
33+
jsContext,
3634
]);
3735
}

packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedStateChangeEvent.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedTouchEvent.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedUpdateEvent.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateChangeHandler.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ReanimatedContext } from 'packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper';
12
import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture';
23
import { State } from '../../../State';
34
import {
@@ -13,7 +14,8 @@ import {
1314

1415
export function getStateChangeHandler<THandlerData>(
1516
handlerTag: number,
16-
callbacks: GestureCallbacks<THandlerData>
17+
callbacks: GestureCallbacks<THandlerData>,
18+
context?: ReanimatedContext<THandlerData>
1719
) {
1820
return (sourceEvent: StateChangeEvent<THandlerData>) => {
1921
'worklet';
@@ -39,6 +41,10 @@ export function getStateChangeHandler<THandlerData>(
3941
runCallback(CALLBACK_TYPE.END, callbacks, event, true);
4042
}
4143
runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, true);
44+
45+
if (context) {
46+
context.lastUpdateEvent = undefined;
47+
}
4248
} else if (
4349
(event.state === State.FAILED || event.state === State.CANCELLED) &&
4450
event.state !== event.oldState
@@ -47,6 +53,10 @@ export function getStateChangeHandler<THandlerData>(
4753
runCallback(CALLBACK_TYPE.END, callbacks, event, false);
4854
}
4955
runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, false);
56+
57+
if (context) {
58+
context.lastUpdateEvent = undefined;
59+
}
5060
}
5161
};
5262
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
Reanimated,
3+
ReanimatedHandler,
4+
} from '../../../handlers/gestures/reanimatedWrapper';
5+
import {
6+
ChangeCalculatorType,
7+
GestureCallbacks,
8+
UnpackedGestureHandlerEvent,
9+
} from '../../types';
10+
import { getStateChangeHandler } from './stateChangeHandler';
11+
import { getTouchEventHandler } from './touchEventHandler';
12+
import { getUpdateHandler } from './updateHandler';
13+
14+
export function useReanimatedEventHandler<THandlerData>(
15+
handlerTag: number,
16+
handlers: GestureCallbacks<THandlerData>,
17+
reanimatedHandler: ReanimatedHandler<THandlerData> | undefined,
18+
changeEventCalculator: ChangeCalculatorType<THandlerData> | undefined
19+
) {
20+
// We don't want to call hooks conditionally, `useEvent` will be always called.
21+
// The only difference is whether we will send events to Reanimated or not.
22+
// The problem here is that if someone passes `Animated.event` as `onUpdate` prop,
23+
// it won't be workletized and therefore `useHandler` will throw. In that case we override it to empty `worklet`.
24+
if (!Reanimated?.isWorkletFunction(handlers.onUpdate)) {
25+
handlers.onUpdate = () => {
26+
'worklet';
27+
// no-op
28+
};
29+
}
30+
31+
const stateChangeCallback = getStateChangeHandler(
32+
handlerTag,
33+
handlers,
34+
reanimatedHandler?.context
35+
);
36+
37+
const updateCallback = getUpdateHandler(
38+
handlerTag,
39+
handlers,
40+
reanimatedHandler?.context,
41+
changeEventCalculator
42+
);
43+
44+
const touchCallback = getTouchEventHandler(handlerTag, handlers);
45+
46+
const callback = (event: UnpackedGestureHandlerEvent<THandlerData>) => {
47+
'worklet';
48+
if ('oldState' in event && event.oldState !== undefined) {
49+
stateChangeCallback(event);
50+
} else if ('allTouches' in event) {
51+
touchCallback(event);
52+
} else {
53+
updateCallback(event);
54+
}
55+
};
56+
57+
const reanimatedEvent = Reanimated?.useEvent(
58+
callback,
59+
[
60+
'onGestureHandlerReanimatedEvent',
61+
'onGestureHandlerReanimatedStateChange',
62+
'onGestureHandlerReanimatedTouchEvent',
63+
],
64+
!!reanimatedHandler?.doDependenciesDiffer
65+
);
66+
67+
return reanimatedEvent;
68+
}

packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from './utils';
1111
import { tagMessage } from '../../utils';
1212
import { BaseGestureConfig, SingleGesture, SingleGestureName } from '../types';
13+
import { Platform } from 'react-native';
1314
import { NativeProxy } from '../NativeProxy';
1415

1516
export function useGesture<THandlerData, TConfig>(
@@ -35,9 +36,7 @@ export function useGesture<THandlerData, TConfig>(
3536
onGestureHandlerStateChange,
3637
onGestureHandlerEvent,
3738
onGestureHandlerTouchEvent,
38-
onReanimatedStateChange,
39-
onReanimatedUpdateEvent,
40-
onReanimatedTouchEvent,
39+
onReanimatedEvent,
4140
onGestureHandlerAnimatedEvent,
4241
} = useGestureCallbacks(tag, config);
4342

@@ -52,12 +51,7 @@ export function useGesture<THandlerData, TConfig>(
5251
throw new Error(tagMessage('Failed to create event handlers.'));
5352
}
5453

55-
if (
56-
config.shouldUseReanimatedDetector &&
57-
(!onReanimatedStateChange ||
58-
!onReanimatedUpdateEvent ||
59-
!onReanimatedTouchEvent)
60-
) {
54+
if (config.shouldUseReanimatedDetector && !onReanimatedEvent) {
6155
throw new Error(tagMessage('Failed to create reanimated event handlers.'));
6256
}
6357

@@ -109,10 +103,24 @@ export function useGesture<THandlerData, TConfig>(
109103
onGestureHandlerStateChange,
110104
onGestureHandlerEvent,
111105
onGestureHandlerTouchEvent,
112-
onReanimatedStateChange,
113-
onReanimatedUpdateEvent,
114-
onReanimatedTouchEvent,
115106
onGestureHandlerAnimatedEvent,
107+
// On web, we're triggering Reanimated callbacks ourselves, based on the type.
108+
// To handle this properly, we need to provide all three callbacks, so we set
109+
// all three to the Reanimated event handler.
110+
// On native, Reanimated handles routing internally based on the event names
111+
// passed to the useEvent hook. We only need to pass it once, so that Reanimated
112+
// can setup its internal listeners.
113+
...(Platform.OS === 'web'
114+
? {
115+
onReanimatedUpdateEvent: onReanimatedEvent,
116+
onReanimatedStateChange: onReanimatedEvent,
117+
onReanimatedTouchEvent: onReanimatedEvent,
118+
}
119+
: {
120+
onReanimatedUpdateEvent: onReanimatedEvent,
121+
onReanimatedStateChange: undefined,
122+
onReanimatedTouchEvent: undefined,
123+
}),
116124
},
117125
gestureRelations,
118126
}),
@@ -123,9 +131,7 @@ export function useGesture<THandlerData, TConfig>(
123131
onGestureHandlerStateChange,
124132
onGestureHandlerEvent,
125133
onGestureHandlerTouchEvent,
126-
onReanimatedStateChange,
127-
onReanimatedUpdateEvent,
128-
onReanimatedTouchEvent,
134+
onReanimatedEvent,
129135
onGestureHandlerAnimatedEvent,
130136
gestureRelations,
131137
]

0 commit comments

Comments
 (0)