[Android] Scrolling performance degradation in development builds
Description
When using Reanimated's useAnimatedScrollHandler or useScrollViewOffset with scrollable components (FlatList/ScrollView), there's a significant performance degradation (stuttering) on Android, but only in development builds. The same code works smoothly in:
- Expo Go on both Android and iOS
- Development builds on iOS
- Both simulator and physical Android devices in Expo Go
- React Native's core Animated API (works smoothly in all builds)
This suggests the issue is specific to Android development builds with Reanimated, as the core Animated API implementation maintains smooth scrolling performance across all build types.
Steps to reproduce
Here's a complete reproduction with all necessary components:
Steps to reproduce:
- Create new Expo project:
npx create-expo-app@latest
- Install dependencies:
npx expo install expo-dev-client
// ScrollAnimationTest.tsx
export default function ScrollAnimationTest() {
const scrollX = useSharedValue(0)
const data = useMemo<ScrollDataType[]>(() => [
{ id: 1, color: '#462255' },
...
], [])
const keyExtractor = useCallback((item: ScrollDataType) => item.id.toString(), []);
const renderItem: ListRenderItem<ScrollDataType> = useCallback(({ item }) => {
return <ScrollDataItem item={item} />
}, [])
const getItemLayout = useCallback((
_data: ArrayLike<ScrollDataType> | null | undefined,
index: number
) => ({
length: SCREEN_WIDTH,
offset: SCREEN_WIDTH * index,
index,
}), []);
const scrollHandler = useAnimatedScrollHandler((event) => {
scrollX.value = event.contentOffset.x;
});
return (
<View style={styles.container}>
<Animated.FlatList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
horizontal
pagingEnabled
getItemLayout={getItemLayout}
contentContainerStyle={styles.listContent}
onScroll={scrollHandler}
scrollEventThrottle={16}
/>
<ScrollIndicatorReanimated data={data} scrollX={scrollX} />
</View>
)
}
// ScrollIndicatorReanimated.tsx
function ScrollIndicatorReanimated({ data, scrollX }: ScrollIndicatorReanimatedProps) {
const dots = useMemo(() => {
return data.map((item, index) => (
<ScrollDotReanimated
key={item.id}
index={index}
scrollX={scrollX}
/>
))
}, [data, scrollX])
return (
<View style={styles.container}>
{dots}
</View>
)
}
export default memo(ScrollIndicatorReanimated)
// ScrollDotReanimated.tsx
const SIZE = 16;
const ScrollDotReanimated = memo(({ index, scrollX }: ScrollDotReanimatedProps) => {
const inputRange = useMemo(() => [
(index - 1) * SCREEN_WIDTH,
index * SCREEN_WIDTH,
(index + 1) * SCREEN_WIDTH,
], [index]);
const animatedStyles = useAnimatedStyle(() => {
return {
opacity: interpolate(
scrollX.value,
inputRange,
[0.5, 1, 0.5],
Extrapolation.CLAMP
),
transform: [{
scale: interpolate(
scrollX.value,
inputRange,
[0.5, 1, 0.5],
Extrapolation.CLAMP
),
}]
};
});
return (
<Animated.View
style={[
styles.dot,
animatedStyles
]}
/>
)
})
export default ScrollDotReanimated
Using Core Animated API (smooth on Android and Ios):
// ScrollAnimationTest.tsx
import { View, Animated as CoreAnimated, StyleSheet, ListRenderItem, Dimensions } from 'react-native'
export default function ScrollAnimationTest() {
const scrollX = useMemo(() => new CoreAnimated.Value(0), []);
const data = useMemo<ScrollDataType[]>(() => [
{ id: 1, color: '#462255' },
...
], [])
const keyExtractor = useCallback((item: ScrollDataType) => item.id.toString(), []);
const renderItem: ListRenderItem<ScrollDataType> = useCallback(({ item }) => {
return <ScrollDataItem item={item} />
}, [])
const scrollHandler = useCallback(
CoreAnimated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: true }
),
[scrollX]
);
return (
<View style={styles.container}>
<CoreAnimated.FlatList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
horizontal
pagingEnabled
getItemLayout={getItemLayout}
contentContainerStyle={styles.listContent}
onScroll={scrollHandler}
scrollEventThrottle={16}
/>
<ScrollIndicatorCoreAnimated data={data} scrollX={scrollX} />
</View>
)
}
// ScrollIndicatorCoreAnimated.tsx
function ScrollIndicatorCoreAnimated({ data, scrollX }: ScrollIndicatorCoreAnimatedProps) {
const dots = useMemo(() => {
return data.map((item, index) => (
<ScrollDotCoreAnimated
key={item.id}
index={index}
scrollX={scrollX}
/>
))
}, [data, scrollX])
return (
<View style={styles.container}>
{dots}
</View>
)
}
export default memo(ScrollIndicatorCoreAnimated)
// ScrollDotCoreAnimated.tsx
const ScrollDotCoreAnimated = memo(({ index, scrollX }: ScrollDotCoreAnimatedType) => {
const inputRange = useMemo(() => [
(index - 1) * SCREEN_WIDTH,
index * SCREEN_WIDTH,
(index + 1) * SCREEN_WIDTH,
], [index]);
const opacity = scrollX.interpolate({
inputRange,
outputRange: [0.5, 1, 0.5],
extrapolate: 'clamp'
});
const scale = scrollX.interpolate({
inputRange,
outputRange: [0.5, 1, 0.5],
extrapolate: 'clamp'
});
return (
<Animated.View
style={[
styles.dot,
{
opacity,
transform: [{ scale }]
}
]}
/>
)
})
export default ScrollDotCoreAnimated
Snack or a link to a repository
https://github.com/NyoriK/scroll-test-app
Reanimated version
3.16.1
React Native version
0.76.7
Platforms
Android
JavaScript runtime
Hermes
Workflow
Expo
Architecture
New Architecture
Build type
Expo Development build
Device
Real device
Device model
Redmi Note 10 pro
Acknowledgements
Yes
I just encounter the same issue on Android and I switch to reanimatedV4 on beta version seem to be ok I am testing on new arch and. react native 0.77.1
Updated
I disabled new arch and it work fine now
Hey guys!
We are aware of the issue. It affects only the New Architecture so that's why switching back to the Old Architecture results in better scrolling performance. We are currently working on a fix for this problem and I hope we figure something out in a near future.
I can confirm this behaviour happen only new arch (DEBUG AND RELASE)
@MatiPl01 Thanks for the team's investigation on this. I want to add that what makes this particularly interesting is the performance difference between Expo Go and development builds. The same code runs smoothly in Expo Go on both Android and iOS, despite Expo Go typically having more overhead than development builds.
This behavior pattern (working in Expo Go but not in dev builds) might offer additional clues about where in the New Architecture integration the performance degradation is occurring. Could this suggest something specific about how Reanimated is being initialized or integrated in development builds vs Expo Go?
@NyoriK
The same code runs smoothly in Expo Go on both Android and iOS, despite Expo Go typically having more overhead than development builds.
Interesting. I don't think there should be any difference in Expo Go builds. It is a bit strange to me. Maybe you ran Expo Go project with an Old Architecture? It shouldn't run faster on New Arch just because of using Expo Go.
@MatiPl01 Hello this mention might disturb you but I have try on new arch release on reanimated latest version it still the same
https://github.com/user-attachments/assets/d3e7984b-7120-4976-b6c9-69dbb97a2bc9
@chanphiromsok
Yeah, I know. We haven't released a fix yet. We started rewriting the core logic in reanimated which we hope to improve performance (at least a bit) and fix some issues we are dealing with now. This will likely land in reanimated 4 in the next months, so you have to be patient and wait for the new version.
I can't promise how much faster reanimated (and, especially scrollTo) will become because we are in the very early stage of this rewrite but we will see once we manage to finish the new implementation.
@chanphiromsok
Yeah, I know. We haven't released a fix yet. We started rewriting the core logic in reanimated which we hope to improve performance (at least a bit) and fix some issues we are dealing with now. This will likely land in reanimated 4 in the next months, so you have to be patient and wait for the new version.
I can't promise how faster reanimated (and, especially
scrollTo) will become because we are in the very early stage of this rewrite but we will see once we manage to finish the new implementation.
Thank you for response
This is the main issue preventing me from updating to the new architecture now. I've tried in expo 52 and now 53. This can be easily reproduced in the react-conf-app using an animated header that animates with the flatlist scrolling. https://github.com/expo/react-conf-app. building a dev client and running on android is almost unusable. Even if it works better in production, there is still a clear difference between old/new arch.
I have noticed if I use useAnimatedScrollHandler with onScroll event in a scrollable view, then my defined event fn will fire with every scrollable view in the whole application. I mean ALL scrollable view, even views without onScroll prop defined. Can it be the source of this problem?
Only on android.
My code looks like this
export const useScrollOffset = () => {
const scrollOffset = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: ({ contentOffset }) => {
scrollOffset.value = contentOffset.y; //this will fire in every scrollable view, even without scrollHandler passed as onScroll prop
},
})
return { scrollHandler, scrollOffset };
};
New architecture enabled. "expo": "53.0.22", "react-native": "0.79.6", "react-native-reanimated": "3.17.4" and "3.19.2"
I believe this issue can be fixed by upgrading to a nightly version of Reanimated (4.2.0-nightly-) and enabling the USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS static feature flag.
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 this link should be particularly useful:
I believe this issue can be fixed by upgrading to a nightly version of Reanimated (
4.2.0-nightly-) and enabling theUSE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITSstatic feature flag.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 this link should be particularly useful:
enabled preventShadowTreeCommitExhaustion is work for me