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

[Android] Scrolling performance degradation in development builds #7034

Open
NyoriK opened this issue Feb 16, 2025 · 1 comment
Open

[Android] Scrolling performance degradation in development builds #7034

NyoriK opened this issue Feb 16, 2025 · 1 comment
Labels
Platform: Android This issue is specific to Android Repro provided A reproduction with a snippet of code, snack or repo is provided

Comments

@NyoriK
Copy link

NyoriK commented Feb 16, 2025

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:

  1. Create new Expo project:
npx create-expo-app@latest
  1. 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

@github-actions github-actions bot added Missing info The user didn't precise the problem enough Repro provided A reproduction with a snippet of code, snack or repo is provided Platform: Android This issue is specific to Android and removed Missing info The user didn't precise the problem enough labels Feb 16, 2025
@chanphiromsok
Copy link

chanphiromsok commented Feb 19, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Platform: Android This issue is specific to Android Repro provided A reproduction with a snippet of code, snack or repo is provided
Projects
None yet
Development

No branches or pull requests

2 participants