software-mansion / react-native-gesture-handler

Declarative API exposing platform native touch and gesture system to React Native.
https://docs.swmansion.com/react-native-gesture-handler/
MIT License
6.13k stars 982 forks source link

new gesture api very slow rendering #3195

Closed Willham12 closed 2 weeks ago

Willham12 commented 3 weeks ago

Description

Bad performance with new gesture pan API. I have a similar circular slider with the same issue and with the old api no issues:

<PanGestureHandler
enabled={!isDisabled}
maxPointers={1}
minDist={1}
onGestureEvent={handleGestureEvent}
onHandlerStateChange={handleGestureEvent}>
...
</PanGestureHandler>

Steps to reproduce

Open the Snack and slider fast from left to right

Snack or a link to a repository

https://snack.expo.dev/qFQCJMaMaZBreK0vOgQ0w

Gesture Handler version

2.20.2

React Native version

0.74.2

Platforms

iOS

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

Paper (Old Architecture)

Build type

Release mode

Device

iOS simulator

Device model

Iphone 15

Acknowledgements

Yes

Willham12 commented 3 weeks ago

Same result with better performance without using a state: https://snack.expo.dev/76p09SjKxZHh977msQHbj And here ist a version with native slider package: https://snack.expo.dev/ViKIaC3sai2I8cVaN_IU2

try to slider very fast from left to right over and over again and you feel the difference.

m-bert commented 2 weeks ago

Hi @Willham12! Using state will significantly decrease performance, that's why you should use SharedValue. Here you can find example that also implements Slider:

Slider example ```jsx import { StyleSheet, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { clamp, Extrapolation, interpolate, measure, useAnimatedRef, useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; const knobSize = 20; const indicatorSize = 10; const getPercentage = (x, width) => { 'worklet'; return clamp((x / width) * 100, 0, 100); }; export default function BalloonSliderLesson() { const x = useSharedValue(0); const knobScale = useSharedValue(0); const aref = useAnimatedRef(); const percentage = useSharedValue(0); const lastPosition = useSharedValue(0); const panGesture = Gesture.Pan() .averageTouches(true) .onBegin((ev) => { x.value = ev.x; }) .onStart((ev) => { knobScale.value = withSpring(1); const { width } = measure(aref); percentage.value = getPercentage(x.value, width); }) .onChange((ev) => { const { width } = measure(aref); x.value = clamp((x.value += ev.changeX), 0, width); percentage.value = getPercentage(x.value, width); }) .onEnd((ev) => { knobScale.value = withSpring(0); lastPosition.value = ev.x; }); const animatedStyle = useAnimatedStyle(() => { return { borderWidth: interpolate( knobScale.value, [0, 1], [knobSize / 2, 2], Extrapolation.CLAMP ), transform: [ { translateX: x.value, }, { scale: knobScale.value + 1, }, ], }; }); return ( ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'space-around', alignItems: 'center', }, knob: { width: knobSize, height: knobSize, borderRadius: knobSize / 2, backgroundColor: '#fff', borderWidth: knobSize / 2, borderColor: 'pink', position: 'absolute', left: -knobSize / 2, top: -knobSize / 3, }, slider: { width: '80%', backgroundColor: 'pink', height: 5, justifyContent: 'center', }, progress: { height: 5, backgroundColor: 'purple', position: 'absolute', }, }); ```

As you can see there are no performance issues:

https://github.com/user-attachments/assets/353d9722-ab6b-4950-b36b-d4cd2f4489cf

Willham12 commented 2 weeks ago

@m-bert but what if the progress depends on a state and this state can't replaced by a sharedValue?

m-bert commented 2 weeks ago

If your state contains progress values (like on a scale from 0 to 100), then you should definitely use SharedValue. If not, you can try useDerivedValue and pass state into return value, e.g.:

import {useDerivedValue} from 'react-native-reanimated';

function App() {
  const [isOpen, setOpen] = React.useState(false);
  const sv = useDerivedValue(() => {
    return isOpen;
  })
// ...
}

Here you can find another example of slider.

Willham12 commented 2 weeks ago

Nah, none of these solutions working for me. I have a very complex ui based on a circular slider and if release the slider onUpdate() still getting fired until the positions are processed. Same if the Component with the Gesture.Pan() are not updating by a state change. Based on the circular slider position i have to calculate a price based on a very complex price model and i don't now why but the new gesture api blocks the whole js thread. No issue with the old API.

m-bert commented 2 weeks ago

We've already showed that it is possible to create smooth slider. I've also provided 2 examples of how to do it. If your app has such level of complexity, then there's not much that we can do. Especially if you will use React states.

Willham12 commented 2 weeks ago

Here is a perfect example, check the log. https://snack.expo.dev/oQjqMz_sLClc9wXmmI9WW With old API the while loop takes almost 0ms.

j-piasecki commented 2 weeks ago

I'm not exactly sure what you mean by checking the log, could you provide an example with the old API where it works as you'd expect to compare to?

Also, I've noticed that you're using .runOnJS modifier on a gesture. This prevents it from being executed on the UI thread and adds a delay between receiving the event and applying the updated values to the views so that they are displayed on the screen. If you want to use a gesture to drive an animation you should do it on the UI thread and use runOnJS function from Reanimated to schedule things that cannot be executed on the UI thread (like state updates).

Coming back to your original snack, you shouldn't use state to drive animations. If you need other components to update in response to a gesture change, consider moving them to use useAnimatedProps or scheduling a state update in response to a shared value change in useAnimatedReaction.

Willham12 commented 2 weeks ago

If i removed a huge part of the underlying components tree then the new gesture api works as expected. Not sure why the legacy gesture api works better.

Willham12 commented 2 weeks ago

After more investigation i notice that the JS frame rate drops down to 0 with new gesture api and with the legacy version its always above 20.

Willham12 commented 1 week ago

@j-piasecki here a video with the legacy PanGestureHandler:

https://github.com/user-attachments/assets/8f59163a-2db7-4459-8c3e-98eebf3c77bf

and here with the new Gesture.Pan() API:

https://github.com/user-attachments/assets/cdce411d-e4bb-448e-8dc9-14a5d63fd5dd

I can't share the real ui but the underlying computation is not simple but with the new gesture api the performance breaks down. panGesture is wrapped into a useMemo and for the progress i'm using useDerivedValue. Also with new gesture api i receive onUpdate events while the knob reached already the final position.

        const panGesture = useMemo(() => {
            return Gesture.Pan()
                .enabled(!isDisabled)
                .minDistance(1)
                .minPointers(1)
                .onBegin((event) => {
...
j-piasecki commented 1 week ago

I understand that you cannot post the entire code, but could you share some more details that may be important here?