Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bad performance if setState while running animation #6247

Closed
doublelam opened this issue Jul 10, 2024 · 6 comments
Closed

Bad performance if setState while running animation #6247

doublelam opened this issue Jul 10, 2024 · 6 comments
Labels
Area: Performance Missing repro This issue need minimum repro scenario Platform: Android This issue is specific to Android

Comments

@doublelam
Copy link

Description

Screen.Recording.2024-07-10.at.11.16.23.mov

Slider.tsx

import React, { useCallback, useImperativeHandle, useState } from 'react';
import { LayoutChangeEvent, StyleProp, ViewStyle } from 'react-native';
import Animated, {
  WithTimingConfig,
  runOnJS,
  useAnimatedReaction,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from 'react-native-reanimated';
import {
  GestureEventPayload,
  PanGestureHandlerEventPayload,
  GestureDetector,
  Gesture,
} from 'react-native-gesture-handler';

import { useTheme } from 'react-native-paper';
import { ThemeType } from '@root/src/styles/theme';
import { getPoint } from '@root/src/functions/worklets';
import styles from './styles';

type Props = {
  style?: StyleProp<ViewStyle>;
  trackStyle?: StyleProp<ViewStyle>;
  pointStyle?: StyleProp<ViewStyle>;
  pointSize?: number;
  step?: number;
  animation?: WithTimingConfig;
  onSlidingComplete?: (v: number) => void;
  onChange?: (v: number) => void;
  minValue?: number;
  maxValue: number;
};

export type SliderRefType = {
  setValue: (v: number) => void;
};
const Slider = React.forwardRef<SliderRefType, Props>(
  (
    {
      style,
      pointStyle,
      trackStyle,
      minValue = 0,
      maxValue,
      step = 0,
      onChange,
      onSlidingComplete,
      pointSize = 20,
      animation,
    },
    ref,
  ) => {
    const pointSharedValue = useSharedValue(0);
    const outputSharedValue = useSharedValue(0);
    const statusSharedValue = useSharedValue(0);
    const originSharedValue = useSharedValue(0);
    const [size, setSize] = useState({ width: 0, height: 0 });
    const theme = useTheme<ThemeType>();

    const setValue = useCallback(
      (val?: number) => {
        if (
          val != null &&
          size.width &&
          size.height &&
          val !== outputSharedValue.value
        ) {
          const left = ((val - minValue) / (maxValue - minValue)) * size.width;
          pointSharedValue.value = left;
          originSharedValue.value = left;
        }
      },
      [size],
    );

    useImperativeHandle(ref, () => ({ setValue }), [setValue]);

    const rehydrateValue = useCallback((duration?: number) => {
      'worklet';

      if (duration) {
        originSharedValue.value = withTiming(pointSharedValue.value, {
          duration,
        });
        return;
      }
      originSharedValue.value = pointSharedValue.value;
    }, []);

    useAnimatedReaction(
      () => outputSharedValue.value,
      (val, prev) => {
        if (prev && prev !== val && onChange) {
          if (onChange) {
            runOnJS(onChange)(val);
          }
        }
      },
    );

    const handleEvent = useCallback(
      (
        e: Readonly<GestureEventPayload & PanGestureHandlerEventPayload>,
        status: 'active' | 'start' | 'end',
      ) => {
        'worklet';

        if (status === 'start') {
          statusSharedValue.value = withSpring(1);
        } else if (status === 'end') {
          statusSharedValue.value = withSpring(0);
        }
        originSharedValue.value = e.x;
        const res = getPoint(e.x, size.width, maxValue, minValue, step);
        if (outputSharedValue.value !== res.value) {
          outputSharedValue.value = res.value;
          if (animation) {
            pointSharedValue.value = withTiming(res.posX, animation);
          } else {
            pointSharedValue.value = res.posX;
          }
        }
        if (status === 'end' && onSlidingComplete) {
          runOnJS(onSlidingComplete)(res.value);
        }
      },
      [pointSize, step, minValue, maxValue, size, animation, onSlidingComplete],
    );

    const eventHandler = Gesture.Pan()
      .hitSlop({ vertical: 20, horizontal: 20 })
      .onBegin(event => {
        handleEvent(event, 'start');
      })
      .onUpdate(event => {
        handleEvent(event, 'active');
      })
      .onFinalize(event => {
        handleEvent(event, 'end');
        rehydrateValue(200);
      });

    const pointAniStyle = useAnimatedStyle(() => {
      return {
        left: originSharedValue.value - pointSize / 2,
        transform: [{ scale: statusSharedValue.value * 0.2 + 1 }],
      };
    });

    const highlightWidthAniStyle = useAnimatedStyle(() => {
      return {
        width: originSharedValue.value,
      };
    });

    const onLayout = useCallback((e: LayoutChangeEvent) => {
      setSize({
        width: e.nativeEvent.layout.width,
        height: e.nativeEvent.layout.height,
      });
    }, []);

    return (
      <Animated.View style={[{ padding: 10 }, style]}>
        <GestureDetector gesture={eventHandler}>
          <Animated.View
            onLayout={onLayout}
            style={[styles.slider, trackStyle]}
          >
            <Animated.View style={[styles.highlight, highlightWidthAniStyle]} />
            <Animated.View
              style={[
                styles.track,
                {
                  width: pointSize,
                  height: pointSize,
                  borderRadius: pointSize / 2,
                  backgroundColor: theme.colors.cardBg,
                },
                pointAniStyle,
                [pointStyle],
              ]}
            />
          </Animated.View>
        </GestureDetector>
      </Animated.View>
    );
  },
);
export default React.memo(Slider);

Usage:

         <List.Section>
            <List.Subheader>
              {trans('STROKE_WIDTH')}: {strokeWidth}
            </List.Subheader>
            <Slider
              step={1}
              ref={swSliderRef}
              style={{ paddingHorizontal: 25 }}
              // onChange={setStrokeWidth}
              minValue={stWidthSet.min}
              maxValue={stWidthSet.max}
            />
          </List.Section>
          <List.Section>
            <List.Subheader>
              {trans('FRAME_RATE')}: {frameRate}fps
            </List.Subheader>
            <Slider
              step={1}
              ref={frSliderRef}
              style={{ paddingHorizontal: 25 }}
              onChange={setFrameRate}
              minValue={frameRateConfig.min}
              maxValue={frameRateConfig.max}
            />
       </List.Section>

The difference between the first and second Slider is that the first Slider does not setState while changing value, so it behaves smoother.

Steps to reproduce

just setState when running animation

Snack or a link to a repository

null

Reanimated version

3.13.0

React Native version

0.74.2

Platforms

Android

JavaScript runtime

Hermes

Workflow

React Native

Architecture

Fabric (New Architecture)

Build type

Debug app & dev bundle

Device

Android emulator

Device model

No response

Acknowledgements

Yes

@github-actions github-actions bot added the Platform: Android This issue is specific to Android label Jul 10, 2024
Copy link

Hey! 👋

The issue doesn't seem to contain a minimal reproduction.

Could you provide a snack or a link to a GitHub repository under your username that reproduces the problem?

@github-actions github-actions bot added the Missing repro This issue need minimum repro scenario label Jul 10, 2024
@MatiPl01
Copy link
Member

Hey!

The runOnJS call doesn't have that huge impact. The real issue stems from the fact that you call setFrameRate, which updates the value in the React state, triggering the re-render of the component. If onChange is called very often, then state updates frequently, causing frame rate to drop.

If you want to show fps when you drag the slider, you can use a trick with the animated TextInput, which will update its content based on the value stored in the shared value without the necessity to re-render the component. See this implementation of the ReText component that uses the animated TextInput.

Alternatively, if you want to make further optimizations, you can get rid off this runOnJS call and pass a worklet function as a onChange callback which will update the shared value used by the ReText component.

@MatiPl01 MatiPl01 added the Close when stale This issue is going to be closed when there is no activity for a while label Jul 16, 2024
@doublelam
Copy link
Author

Hi, thank you for your suggestion but this is a temporary solution for my current issue. I did not face this issue until I upgraded react-native from 0.73 to 0.74, enabled new Architecture and upgraded react-native-reanimated from 3.7.0 to 3.13.0.

@github-actions github-actions bot removed the Close when stale This issue is going to be closed when there is no activity for a while label Jul 18, 2024
@MatiPl01
Copy link
Member

Hi, thank you for your suggestion but this is a temporary solution for my current issue. I did not face this issue until I upgraded react-native from 0.73 to 0.74, enabled new Architecture and upgraded react-native-reanimated from 3.7.0 to 3.13.0.

Thank you for details. Could you please upgrade to 3.14.0 and see if the issue still persists? There was a bug introduced in 3.9.0 which is resolved in 3.14.0, which caused loads of complaints acout performance and animations instability.

@doublelam
Copy link
Author

doublelam commented Jul 18, 2024

I test it in release mode, it behaves better than in debug mode. Guess may be because of my device. I will improve my code to add a debounce function that should help resolve my problem. Thank you!

@MrMoovf
Copy link

MrMoovf commented Oct 9, 2024

Does anyone have a solution for this? I am on RN 0.74.5 and Reanimated version 3.10.1. I cant update to a higher version of reanimated, since it has to be installed with Expo. Running the "npx expo install react-native-reanimated" only seems to allow for the 3.10.1 version.
If i force the higher version i get an error saying there is an incompatibility between JS and Native part of the code.

Please help this is a really big issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Performance Missing repro This issue need minimum repro scenario Platform: Android This issue is specific to Android
Projects
None yet
Development

No branches or pull requests

4 participants