react-native: Scrolling a 'moving' FlatList causes it to stutter or block scrolling completely on Android
Description
When you create a FlatList that moves as it’s being scrolled (change in offset for the FlatList is a change in top
property of FlatList’s style) it stutters heavily. This behavior is much less disruptive when movement is scaled down compared to scrolling and becomes app-breaking when upscaled. See provided videos:
- Multiplier: 1.0 mult1.webm
- Mutliplier: 2.0 mult2.webm
- Multiplier: 0.5 mult0.5.webm
It was originally an issue in react-native-reanimated but after digging into it I narrowed it down to react-native
and rewrote the code to not use react-native-reanimated. It seems to had been in the code for a long time, since this posts that dates 3 years back is about the same thing.
I checked it on versions 0.72, 0.71 and 0.70. On both Fabric and Paper and the result is always the same (although it feels a bit like Fabric behaviour is worse).
iOS works properly on each version too.
It seems like there is a problem when calculating layout and touch events - that’s what I conducted due to multiplier
making it better or worse. After moving a FlatList twice the distance it scrolled and having the component moved React Native seems to ‘think’ that the finger is now further than it should be and lowers the offset? That’s just my guess.
React Native Version
0.72.3
Output of npx react-native info
System: OS: macOS 13.4.1 CPU: (10) arm64 Apple M2 Pro Memory: 49.31 MB / 16.00 GB Shell: version: “5.9” path: /bin/zsh Binaries: Node: version: 18.16.0 path: /var/folders/jg/m839qn593nn7w_h3n0r9k25c0000gn/T/yarn–1689593442476-0.9098900178237601/node Yarn: version: 1.22.19 path: /var/folders/jg/m839qn593nn7w_h3n0r9k25c0000gn/T/yarn–1689593442476-0.9098900178237601/yarn npm: version: 9.5.1 path: ~/.nvm/versions/node/v18.16.0/bin/npm Watchman: version: 2023.06.12.00 path: /opt/homebrew/bin/watchman Managers: CocoaPods: version: 1.12.1 path: /Users/user/.rbenv/shims/pod SDKs: iOS SDK: Platforms: - DriverKit 22.4 - iOS 16.4 - macOS 13.3 - tvOS 16.4 - watchOS 9.4 Android SDK: Not Found IDEs: Android Studio: 2022.1 AI-221.6008.13.2211.9619390 Xcode: version: 14.3.1/14E300c path: /usr/bin/xcodebuild Languages: Java: version: 11.0.19 path: /usr/bin/javac Ruby: version: 3.2.2 path: /Users/user/.rbenv/shims/ruby npmPackages: “@react-native-community/cli”: Not Found react: installed: 18.2.0 wanted: 18.2.0 react-native: installed: 0.72.3 wanted: 0.72.3 react-native-macos: Not Found npmGlobalPackages: “react-native”: Not Found Android: hermesEnabled: true newArchEnabled: true iOS: hermesEnabled: true newArchEnabled: false
Steps to reproduce
Just need to run the provided code snippet.
Snack, code example, screenshot, or link to a repository
https://github.com/tjzel/ReactNativeMovingFlatList
import React from 'react';
import {Dimensions, FlatList, StyleSheet, Text, View} from 'react-native';
const {height: SCREEN_HEIGHT} = Dimensions.get('screen');
const data = Array.from({length: 30}, (_, index) => ({
id: `${index + 1}`,
text: `Item ${index + 1}`,
}));
const multiplier = 1.0;
export default function App() {
const [animatedVerticalScroll, setAnimatedVerticalScroll] = React.useState(0);
// used to detect stutters
const previousValue = React.useRef(0);
const listAnimatedStyle = {
top:
animatedVerticalScroll * multiplier > SCREEN_HEIGHT * 0.7
? 0
: SCREEN_HEIGHT * 0.7 - animatedVerticalScroll * multiplier,
};
const renderItem = ({item}) => (
<View style={styles.item}>
<Text>{item.text}</Text>
</View>
);
const scrollHandler = event => {
// logger to detect if there was a stutter
if (previousValue.current > event.nativeEvent.contentOffset.y) {
console.log(
`\nScrolling stuttered!\nPrevious offset was ${previousValue.current}\nCurrent offset is ${event.nativeEvent.contentOffset.y}\n`,
);
} else {
console.log(event.nativeEvent.contentOffset.y);
}
setAnimatedVerticalScroll(event.nativeEvent.contentOffset.y);
previousValue.current = event.nativeEvent.contentOffset.y;
};
return (
<View style={styles.container}>
<View style={styles.containerBehind}>
<Text>Multiplier: {multiplier}</Text>
</View>
<FlatList
data={data}
renderItem={renderItem}
style={[styles.listContainer, listAnimatedStyle]}
onScroll={scrollHandler}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
containerBehind: {
width: '100%',
height: '100%',
flex: 1,
backgroundColor: 'red',
alignItems: 'center',
justifyContent: 'center',
},
listContainer: {
top: 0,
left: 0,
position: 'absolute',
zIndex: 10,
width: '100%',
height: '100%',
backgroundColor: 'green',
},
item: {
height: 50,
},
});
About this issue
- Original URL
- State: open
- Created a year ago
- Reactions: 3
- Comments: 21 (11 by maintainers)
Commits related to this issue
- Remove default 50ms Scroll Event Throttling in VirtualizedList Summary: This is specific to the scenario, and device, and time-based sampling as implemented on Android may inherently create stutters ... — committed to NickGerleman/react-native by NickGerleman a year ago
- Remove default 50ms Scroll Event Throttling in VirtualizedList (#38648) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38648 This is specific to the scenario, and devi... — committed to NickGerleman/react-native by NickGerleman a year ago
- Remove default 50ms Scroll Event Throttling in VirtualizedList (#38648) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38648 This is specific to the scenario, and devi... — committed to NickGerleman/react-native by NickGerleman a year ago
- Remove default 50ms Scroll Event Throttling in VirtualizedList (#38648) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38648 https://github.com/facebook/react-native/p... — committed to NickGerleman/react-native by NickGerleman a year ago
- Remove default 50ms Scroll Event Throttling in VirtualizedList (#38648) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38648 https://github.com/facebook/react-native/p... — committed to NickGerleman/react-native by NickGerleman a year ago
- Remove default 50ms Scroll Event Throttling in VirtualizedList (#38648) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38648 https://github.com/facebook/react-native/p... — committed to NickGerleman/react-native by NickGerleman a year ago
- Remove default 50ms Scroll Event Throttling in VirtualizedList (#38648) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38648 https://github.com/facebook/react-native/p... — committed to facebook/react-native by NickGerleman a year ago
It still persist. Is there any update @NickGerleman
Hey @NickGerleman, have you had the time to look into this? I built an app with nightly RN version and it seems the issue is still there.
@efstathiosntonas #38475 is meant to support props that allow ScrollView to get throttled. If not used it should have no effect on ScrollView. I’ll take a look to see why it’s making any differences here.
After building from source with the above PR included it’s even worse @ryancat:
demo video
https://github.com/facebook/react-native/assets/717975/5237186c-4451-4a83-81d8-d69d5a26ffdf
Anecdotally, I am aware of some code which implements a similar scenario and it seems to work okay on Android. Their implementation looks like:
top
might cause relayout?)Animated.FlatList
and theAnimated.event
APIs. Can natively accelerate the transforms without hitting JS I thinkAnimated.FlatList
does overridescrollEventThrottle
to effectively disable it, as well.