Skip to content

Commit 80006ff

Browse files
authored
[General] Reduce number of created closures (#3853)
## Description I noticed that `useGestureCallbacks` still took significant time to run, despite not doing that much. I narrowed it down to the fact that it created a lot of closures for different events, while we only need two: one for JS and the other for Reanimated. This PR refactors how the state machine is created for gestures to reduce the number of closures created to 2 per `useGestureCallbacks` call. ## Test plan <details> <summary>Tested on the stress-test</summary> ```jsx import React, { Profiler, useEffect, useRef } from 'react'; import { ScrollView, StyleSheet, View } from 'react-native'; import { Gesture, GestureDetector, usePanGesture } 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(500).fill(null).map((_, i) => `Item ${i + 1}`); function Item() { const translateX = useSharedValue(0); const style = useAnimatedStyle(() => { return { transform: [{ translateX: translateX.value }], }; }); const pan = usePanGesture({ disableReanimated: false, onUpdate: (event) => { 'worklet'; console.log('pan onUpdate', event.changeX); }, onActivate: () => { 'worklet'; console.log('pan onStart', _WORKLET); }, onDeactivate: () => { 'worklet'; console.log('pan onEnd'); }, onBegin: () => { 'worklet'; console.log('pan onBegin'); }, onFinalize: () => { 'worklet'; console.log('pan onFinalize'); }, onTouchesDown: () => { 'worklet'; console.log('pan onTouchesDown'); }, }); return ( <View style={{ height: 80, padding: 16, backgroundColor: 'gray' }}> <GestureDetector gesture={pan}> {/* <View collapsable={false} style={{opacity: 0.5}}> */} <Animated.View style={[ { backgroundColor: 'red', height: '100%', aspectRatio: 1 }, style, ]} /> {/* </View> */} </GestureDetector> </View> ); } 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(false); 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|After| |-|-|-| |Reanimated|549ms|519ms| |No reanimated|472ms|460ms|
1 parent b27c7c4 commit 80006ff

File tree

10 files changed

+247
-228
lines changed

10 files changed

+247
-228
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import {
2+
flattenAndFilterEvent,
3+
isEventForHandlerWithTag,
4+
maybeExtractNativeEvent,
5+
runCallback,
6+
touchEventTypeToCallbackType,
7+
} from '../utils';
8+
import { tagMessage } from '../../../utils';
9+
import { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper';
10+
import {
11+
ChangeCalculatorType,
12+
GestureCallbacks,
13+
GestureHandlerEventWithHandlerData,
14+
GestureStateChangeEventWithHandlerData,
15+
GestureUpdateEventWithHandlerData,
16+
} from '../../types';
17+
import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture';
18+
import { State } from '../../../State';
19+
import { TouchEventType } from '../../../TouchEventType';
20+
import { GestureTouchEvent } from '../../../handlers/gestureHandlerCommon';
21+
22+
function handleStateChangeEvent<THandlerData>(
23+
eventWithData: GestureStateChangeEventWithHandlerData<THandlerData>,
24+
callbacks: GestureCallbacks<THandlerData>,
25+
context: ReanimatedContext<THandlerData>
26+
) {
27+
'worklet';
28+
const { oldState, state } = eventWithData;
29+
const event = flattenAndFilterEvent(eventWithData);
30+
31+
if (oldState === State.UNDETERMINED && state === State.BEGAN) {
32+
runCallback(CALLBACK_TYPE.BEGAN, callbacks, event);
33+
} else if (
34+
(oldState === State.BEGAN || oldState === State.UNDETERMINED) &&
35+
state === State.ACTIVE
36+
) {
37+
runCallback(CALLBACK_TYPE.START, callbacks, event);
38+
} else if (oldState !== state && state === State.END) {
39+
if (oldState === State.ACTIVE) {
40+
runCallback(CALLBACK_TYPE.END, callbacks, event, true);
41+
}
42+
runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, true);
43+
44+
if (context) {
45+
context.lastUpdateEvent = undefined;
46+
}
47+
} else if (
48+
(state === State.FAILED || state === State.CANCELLED) &&
49+
state !== oldState
50+
) {
51+
if (oldState === State.ACTIVE) {
52+
runCallback(CALLBACK_TYPE.END, callbacks, event, false);
53+
}
54+
runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, false);
55+
56+
if (context) {
57+
context.lastUpdateEvent = undefined;
58+
}
59+
}
60+
}
61+
62+
export function handleUpdateEvent<THandlerData>(
63+
eventWithData: GestureUpdateEventWithHandlerData<THandlerData>,
64+
handlers: GestureCallbacks<THandlerData>,
65+
changeEventCalculator: ChangeCalculatorType<THandlerData> | undefined,
66+
context: ReanimatedContext<THandlerData>
67+
) {
68+
'worklet';
69+
const eventWithChanges = changeEventCalculator
70+
? changeEventCalculator(
71+
eventWithData,
72+
context ? context.lastUpdateEvent : undefined
73+
)
74+
: eventWithData;
75+
76+
const event = flattenAndFilterEvent(eventWithChanges);
77+
78+
// This should never happen, but since we don't want to call hooks conditionally, we have to mark
79+
// context as possibly undefined to make TypeScript happy.
80+
if (!context) {
81+
throw new Error(tagMessage('Event handler context is not defined'));
82+
}
83+
84+
runCallback(CALLBACK_TYPE.UPDATE, handlers, event);
85+
86+
context.lastUpdateEvent = eventWithData;
87+
}
88+
89+
export function handleTouchEvent<THandlerData>(
90+
event: GestureTouchEvent,
91+
handlers: GestureCallbacks<THandlerData>
92+
) {
93+
'worklet';
94+
95+
if (event.eventType !== TouchEventType.UNDETERMINED) {
96+
runCallback(touchEventTypeToCallbackType(event.eventType), handlers, event);
97+
}
98+
}
99+
100+
export function eventHandler<THandlerData>(
101+
handlerTag: number,
102+
sourceEvent: GestureHandlerEventWithHandlerData<THandlerData>,
103+
handlers: GestureCallbacks<THandlerData>,
104+
changeEventCalculator: ChangeCalculatorType<THandlerData> | undefined,
105+
jsContext: ReanimatedContext<THandlerData>,
106+
dispatchesAnimatedEvents: boolean
107+
) {
108+
'worklet';
109+
const eventWithData = maybeExtractNativeEvent(sourceEvent);
110+
111+
if (!isEventForHandlerWithTag(handlerTag, eventWithData)) {
112+
return;
113+
}
114+
115+
if ('oldState' in eventWithData && eventWithData.oldState !== undefined) {
116+
handleStateChangeEvent(eventWithData, handlers, jsContext);
117+
} else if ('allTouches' in eventWithData) {
118+
handleTouchEvent(eventWithData, handlers);
119+
} else if (!dispatchesAnimatedEvents) {
120+
handleUpdateEvent(
121+
eventWithData,
122+
handlers,
123+
changeEventCalculator,
124+
jsContext
125+
);
126+
}
127+
}

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

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

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

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

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

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper';
2+
import {
3+
BaseGestureConfig,
4+
GestureCallbacks,
5+
GestureHandlerEventWithHandlerData,
6+
} from '../../types';
7+
import { useMemo } from 'react';
8+
import { eventHandler } from './eventHandler';
9+
10+
export function useGestureEventHandler<THandlerData, TConfig>(
11+
handlerTag: number,
12+
handlers: GestureCallbacks<THandlerData>,
13+
config: BaseGestureConfig<THandlerData, TConfig>
14+
) {
15+
const jsContext: ReanimatedContext<THandlerData> = useMemo(() => {
16+
return {
17+
lastUpdateEvent: undefined,
18+
};
19+
}, []);
20+
21+
return useMemo(() => {
22+
return (event: GestureHandlerEventWithHandlerData<THandlerData>) => {
23+
eventHandler(
24+
handlerTag,
25+
event,
26+
handlers,
27+
config.changeEventCalculator,
28+
jsContext,
29+
!!config.dispatchesAnimatedEvents
30+
);
31+
};
32+
}, [
33+
handlerTag,
34+
handlers,
35+
config.changeEventCalculator,
36+
config.dispatchesAnimatedEvents,
37+
jsContext,
38+
]);
39+
}

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

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useMemo } from 'react';
12
import {
23
Reanimated,
34
ReanimatedHandler,
@@ -7,53 +8,47 @@ import {
78
GestureCallbacks,
89
UnpackedGestureHandlerEventWithHandlerData,
910
} from '../../types';
10-
import { getStateChangeHandler } from './stateChangeHandler';
11-
import { getTouchEventHandler } from './touchEventHandler';
12-
import { getUpdateHandler } from './updateHandler';
11+
import { eventHandler } from './eventHandler';
12+
13+
const workletNOOP = () => {
14+
'worklet';
15+
// no-op
16+
};
1317

1418
export function useReanimatedEventHandler<THandlerData>(
1519
handlerTag: number,
1620
handlers: GestureCallbacks<THandlerData>,
1721
reanimatedHandler: ReanimatedHandler<THandlerData> | undefined,
1822
changeEventCalculator: ChangeCalculatorType<THandlerData> | undefined
1923
) {
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-
);
24+
const workletizedHandlers = useMemo(() => {
25+
// We don't want to call hooks conditionally, `useEvent` will be always called.
26+
// The only difference is whether we will send events to Reanimated or not.
27+
// The problem here is that if someone passes `Animated.event` as `onUpdate` prop,
28+
// it won't be workletized and therefore `useHandler` will throw. In that case we override it to empty `worklet`.
29+
if (!Reanimated?.isWorkletFunction(handlers.onUpdate)) {
30+
return {
31+
...handlers,
32+
onUpdate: workletNOOP,
33+
};
34+
}
4335

44-
const touchCallback = getTouchEventHandler(handlerTag, handlers);
36+
return handlers;
37+
}, [handlers]);
4538

4639
const callback = (
4740
event: UnpackedGestureHandlerEventWithHandlerData<THandlerData>
4841
) => {
4942
'worklet';
50-
if ('oldState' in event && event.oldState !== undefined) {
51-
stateChangeCallback(event);
52-
} else if ('allTouches' in event) {
53-
touchCallback(event);
54-
} else {
55-
updateCallback(event);
56-
}
43+
eventHandler(
44+
handlerTag,
45+
event,
46+
workletizedHandlers,
47+
changeEventCalculator,
48+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
49+
reanimatedHandler?.context!,
50+
false
51+
);
5752
};
5853

5954
const reanimatedEvent = Reanimated?.useEvent(

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

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,11 @@ export function useGesture<THandlerData, TConfig>(
3333

3434
// TODO: Call only necessary hooks depending on which callbacks are defined (?)
3535
const {
36-
onGestureHandlerStateChange,
3736
onGestureHandlerEvent,
38-
onGestureHandlerTouchEvent,
3937
onReanimatedEvent,
4038
onGestureHandlerAnimatedEvent,
4139
} = useGestureCallbacks(tag, config);
4240

43-
// This should never happen, but since we don't want to call hooks conditionally,
44-
// we have to mark these as possibly undefined to make TypeScript happy.
45-
if (
46-
!onGestureHandlerStateChange ||
47-
// If onUpdate is an AnimatedEvent, `onGestureHandlerEvent` will be undefined and vice versa.
48-
(!onGestureHandlerEvent && !onGestureHandlerAnimatedEvent) ||
49-
!onGestureHandlerTouchEvent
50-
) {
51-
throw new Error(tagMessage('Failed to create event handlers.'));
52-
}
53-
5441
if (config.shouldUseReanimatedDetector && !onReanimatedEvent) {
5542
throw new Error(tagMessage('Failed to create reanimated event handlers.'));
5643
}
@@ -95,14 +82,14 @@ export function useGesture<THandlerData, TConfig>(
9582
}, [tag, config, type]);
9683

9784
return useMemo(
98-
() => ({
85+
(): SingleGesture<THandlerData, TConfig> => ({
9986
tag,
10087
type,
10188
config,
10289
detectorCallbacks: {
103-
onGestureHandlerStateChange,
104-
onGestureHandlerEvent,
105-
onGestureHandlerTouchEvent,
90+
onGestureHandlerStateChange: onGestureHandlerEvent,
91+
onGestureHandlerEvent: onGestureHandlerEvent,
92+
onGestureHandlerTouchEvent: onGestureHandlerEvent,
10693
onGestureHandlerAnimatedEvent,
10794
// On web, we're triggering Reanimated callbacks ourselves, based on the type.
10895
// To handle this properly, we need to provide all three callbacks, so we set
@@ -128,9 +115,7 @@ export function useGesture<THandlerData, TConfig>(
128115
tag,
129116
type,
130117
config,
131-
onGestureHandlerStateChange,
132118
onGestureHandlerEvent,
133-
onGestureHandlerTouchEvent,
134119
onReanimatedEvent,
135120
onGestureHandlerAnimatedEvent,
136121
gestureRelations,

0 commit comments

Comments
 (0)