react-native-reanimated icon indicating copy to clipboard operation
react-native-reanimated copied to clipboard

scrollTo performance issues on the New Architecture when multiple views are animated at the same time

Open MatiPl01 opened this issue 10 months ago • 17 comments

Description

Problem description

I was looking into possible improvements of auto scroll performance in my react-native-sortables library. Performance is fine on the Old Architecture, but on the New Architecture it is horrible. Apart from massive FPS drops and noticeably higher memory usage (might be related to RN, memory is not a big problem), there are also a some other problems:

  • scrollTo runs synchronously, whilst style updates are asynchronous, which results in the noticeable delay between ScrollView position update and the view position update,
  • this delay is higher when the number of animated views is increased (probably because of the style updates batching system and conflicts between Reanimated and RN commits)

Example recording

Old Architecture (Paper) New Architecture (Fabric)

What I tried

To fix the problem with delay between view update and the ScrollView position update I tried to read the view position with the synchronous measure function from Reanimated and scroll the ScrollView only after the view was transitioned. This approach also doesn't work as, likely, the new view position is applied somewhere between frames in the useFrameCallback and there is still some delay between transformation of the view and scrolling the ScrollView.

Final remarks

The issue is less visible in the release build on the real device, likely because of more FPS, which result in more frequent commits and lower delays.

Steps to reproduce

Copy this code to your app and run it on the Old and the New Architecture:

import { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated, {
  measure,
  scrollTo,
  useAnimatedReaction,
  useAnimatedRef,
  useAnimatedStyle,
  useFrameCallback,
  useSharedValue,
  withRepeat,
  withSequence,
  withTiming
} from 'react-native-reanimated';

const ZERO_VECTOR = { x: 0, y: 0 };

export default function PlaygroundExample() {
  const animatedRef = useAnimatedRef<Animated.ScrollView>();
  const itemRef = useAnimatedRef<Animated.View>();
  const containerRef = useAnimatedRef<Animated.View>();

  const startScrollOffset = useSharedValue<number | null>(null);
  const startPosition = useSharedValue(ZERO_VECTOR);
  const currentPosition = useSharedValue(ZERO_VECTOR);
  const newScrollOffset = useSharedValue(0);

  const views = useMemo(() => {
    return Array.from({ length: 100 }, (_, index) => {
      const blueShade = `hsl(210, 80%, ${100 - index}%)`;
      return <Item color={blueShade} name={`Item ${index + 1}`} key={index} />;
    });
  }, []);

  useAnimatedReaction(
    () => ({
      offsetDiff: newScrollOffset.value - (startScrollOffset.value ?? 0)
    }),
    ({ offsetDiff }) => {
      currentPosition.value = {
        x: startPosition.value.x,
        y: startPosition.value.y + offsetDiff
      };
    }
  );

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        { translateX: currentPosition.value.x },
        { translateY: currentPosition.value.y }
      ]
    };
  });

  useFrameCallback(() => {
    const scrollableMeasurements = measure(animatedRef);
    const itemMeasurements = measure(itemRef);
    const containerMeasurements = measure(containerRef);
    if (
      !scrollableMeasurements ||
      !itemMeasurements ||
      !containerMeasurements
    ) {
      return;
    }

    const itemY = itemMeasurements.pageY - containerMeasurements.pageY;

    const offsetDiff = itemY - (startPosition.value.y ?? 0);

    newScrollOffset.value = newScrollOffset.value + 5;
    scrollTo(animatedRef, 0, (offsetDiff + newScrollOffset.value) / 2, false);
  });

  return (
    <Animated.ScrollView ref={animatedRef} contentContainerStyle={{ gap: 20 }}>
      <View ref={containerRef}>
        {views}
        <Animated.View
          ref={itemRef}
          style={[styles.absoluteItem, animatedStyle]}
        />
      </View>
    </Animated.ScrollView>
  );
}

function Item({ color, name }: { name: string; color: string }) {
  const heavyStyle = useAnimatedStyle(() => {
    return {
      left: withRepeat(withSequence(withTiming(100), withTiming(0)), -1, true)
    };
  });

  return (
    <Animated.View
      style={[styles.item, heavyStyle, { backgroundColor: color }]}>
      <Text style={styles.text}>{name}</Text>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  item: {
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'red',
    height: 100
  },
  absoluteItem: {
    position: 'absolute',
    backgroundColor: 'red',
    height: 100,
    width: 100
  },
  text: {
    fontWeight: 'bold'
  }
});

Snack or a link to a repository

https://github.com/MatiPl01/react-native-sortables <- you can test it here as well on the Auto Scroll example screen

Reanimated version

3.16.6

React Native version

0.76.5

Platforms

iOS, Android

JavaScript runtime

None

Workflow

None

Architecture

Fabric (New Architecture)

Build type

None

Device

None

Device model

No response

Acknowledgements

Yes

MatiPl01 avatar Feb 09 '25 18:02 MatiPl01

Hey! 👋

It looks like you've omitted a few important sections from the issue template.

Please complete Description section.

github-actions[bot] avatar Feb 09 '25 18:02 github-actions[bot]

Is there any roadmap or rough estimation of when this will be picked up?

fbeccaceci avatar Feb 27 '25 15:02 fbeccaceci

This is a big issue for us, as our collapsible tab component became unusable after switching to Fabric. Anything that uses scrollTo and useSharedValue to update something synchronously and scroll the view stutters, sometimes even crashes.

hirbod avatar Mar 11 '25 16:03 hirbod

Simplified reproduction example:

Tested on RN 0.78 / Reanimated 3.17.1

No scrolling scrollTo maunal scrolling
Code snippet
import { StyleSheet, Text } from 'react-native';
import Animated, {
  scrollTo,
  useAnimatedRef,
  useAnimatedStyle,
  useFrameCallback,
  useSharedValue,
  withRepeat,
  withSequence,
  withTiming,
} from 'react-native-reanimated';

const COUNT = 200;

export default function PlaygroundExample() {
  const animatedRef = useAnimatedRef<Animated.ScrollView>();
  const sv = useSharedValue(0);

  useFrameCallback(() => {
    sv.value++;
    scrollTo(animatedRef, 0, sv.value, false);
  });

  return (
    <Animated.ScrollView ref={animatedRef}>
      {Array.from({ length: COUNT }, (_, index) => (
        <Item
          color={`hsl(210, 80%, ${COUNT - index}%)`}
          name={`Item ${index + 1}`}
          key={index}
        />
      ))}
    </Animated.ScrollView>
  );
}

function Item({ color, name }: { name: string; color: string }) {
  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          translateX: withRepeat(
            withSequence(withTiming(100), withTiming(0)),
            -1,
            true
          ),
        },
      ],
    };
  });

  return (
    <Animated.View
      style={[styles.item, animatedStyle, { backgroundColor: color }]}>
      <Text style={styles.text}>{name}</Text>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  item: {
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'red',
    height: 100,
  },
  text: {
    fontWeight: 'bold',
  },
});

It seems that the real problem is the fact that each ScrollView position change is committed to the ShadowTree, no matter if this position change is triggered manually or by the scrollTo method call. We set the scrollEventThrottle={1} on the ScrollView, so scroll events are emitted very often, causing very frequent ShadowTree commits.

MatiPl01 avatar Mar 13 '25 17:03 MatiPl01

Even though your videos show the performance regression clearly, it’s even worse in our app. In my video, I have a gesture detector in the header area that translates the header and needs to trigger scrollTo. When I scroll the view manually, everything is fine (you can see that in the middle of the video when it’s smooth), but when I need to sync the movement and translation with the scroll view, it’s like 2fps. I see all the events in the log, but scrollTo just can’t keep up.

https://github.com/user-attachments/assets/de44f89d-b6e5-4cd5-b760-84a9743b9c6b

Pretty sure it’s the same issue, but mine feels even worse when combined with the gesture. Android is also affected, but iOS took the biggest hit compared to the old architecture.

hirbod avatar Mar 13 '25 19:03 hirbod

@hirbod Which RN version are you on? This issue could be related to this PR that landed in RN 0.77. We might need to revamp the scrollTo implementation on our side.

bartlomiejbloniarz avatar Mar 24 '25 11:03 bartlomiejbloniarz

It turns out that the enableFixForViewCommandRace flag default value has been removed by mistake in RN 0.78. I guess that's why the recording from @hirbod is so bad. The recordings from @MatiPl01 come from RN 76 and RN 78. That's why they are not as bad.

This means that the issue reported by @hirbod is a different issue, that was a problem in RN 0.77 and will become a problem in RN 0.79.

bartlomiejbloniarz avatar Mar 24 '25 12:03 bartlomiejbloniarz

Hi there, sorry for the late response. Yes, we're at 0.77.1

hirbod avatar Mar 26 '25 16:03 hirbod

I have the same issue. Upgrading to RN 0.79.1 improved the performance a bit but it's still too slow to go production.

villemuittari avatar Apr 21 '25 08:04 villemuittari

same issue 0.77.2 did u find any solution ? 👀

grkemtneri avatar Apr 22 '25 08:04 grkemtneri

Are there any new findings here regarding this issue?

hirbod avatar Apr 22 '25 23:04 hirbod

Experiencing the exact same issue.

kjartanium avatar Apr 25 '25 13:04 kjartanium

Same issue, 0.77.2, reanimated 3.17.5

genesiscz avatar May 01 '25 00:05 genesiscz

same issue , rn 0.79.2 , reanimated 3.17.4

https://github.com/user-attachments/assets/f58c71ee-283e-436e-922e-9ecbe54d123f

liixing avatar May 07 '25 04:05 liixing

Any updates here? :)

DSKonstantin avatar May 16 '25 16:05 DSKonstantin

BTW related: #7460

genesiscz avatar May 26 '25 12:05 genesiscz

Same issue

Jianlong-Nie avatar Jun 24 '25 22:06 Jianlong-Nie

Same issue

Jianlong-Nie avatar Jul 02 '25 06:07 Jianlong-Nie

same issue RN 0.78.3, reanimated 3.18.0

artymir avatar Jul 17 '25 07:07 artymir

same issues on RN 0.79.5, Reanimated 3.18.0

oleksandr-dziuban avatar Jul 24 '25 14:07 oleksandr-dziuban

As others are saying, this is still an issue and blocker for migrating to the new react architecture. Can this be prioritized?

aaronabf avatar Jul 24 '25 17:07 aaronabf

If you're experiencing performance regressions of animations after enabling the New Architecture, please make sure to check out this page we recently added in Reanimated docs:

In the context of this issue, I believe these two links should be particularly useful:

tomekzaw avatar Oct 28 '25 11:10 tomekzaw