Description
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 betweenScrollView
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) |
---|---|
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-02-09.at.15.02.03.mp4 |
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-02-09.at.15.00.26.mp4 |
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