react-native-gesture-handler icon indicating copy to clipboard operation
react-native-gesture-handler copied to clipboard

ReanimatedSwipeable | slow performance scrolling

Open erie-e9 opened this issue 11 months ago • 8 comments

Description

FPS down when I'm using ReanimatedSwipeable from import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable' in each item Items are not been loaded until fetch from API is done, so this is not a load data issue. If I replace it for Swipeable from import Swipeable from 'react-native-gesture-handler/Swipeable' all work great

This issue is happening in Dev and Release apps and using Hermes + RN's New arch.

Note: I applied FlatList performance strategies and tested it with/without them and the issue is still happening.

Steps to reproduce

  1. Run app on real device (issue is most visible on Android devices).
  2. Turn on Perf Monitor.
  3. Create a List with items using FlatList
  4. Add Swipeable actions for each item.
  5. Scroll down/up list.

Snack or a link to a repository

https://github.com

Gesture Handler version

2.21.0, 2.21.1 and 2.21.2

React Native version

0.76.5

Platforms

Android, iOS

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

Fabric (New Architecture)

Build type

Release mode

Device

Real device

Device model

Huawei P30 Pro, iPhone 16 Pro Max

Acknowledgements

Yes

erie-e9 avatar Dec 21 '24 23:12 erie-e9

Hey! 👋

The issue doesn't seem to contain a minimal reproduction.

Could you provide a snack or a link to a GitHub repository under your username that reproduces the problem?

github-actions[bot] avatar Dec 21 '24 23:12 github-actions[bot]

Hi, I tested both the legacy, and the reanimated Swipeable on the main branch, using the FlashList and useBenchmark from @shopify/flash-list. I didn't notice any performance advantage in using the legacy Swipeable over the reanimated Swipeable, both averaged 30fps on Samsung S21 Ultra.

Test code - press to uncollapse
import React, { Text, StyleSheet, View, Alert } from 'react-native';

import ReanimatedSwipeable from 'react-native-gesture-handler/ReanimatedSwipeable';
import { FlashList, ListRenderItem, useBenchmark } from '@shopify/flash-list';
import { memo, useRef } from 'react';
import {
  GestureHandlerRootView,
  // Swipeable as LegacySwipeable,
} from 'react-native-gesture-handler';

function RightAction() {
  return <Text style={styles.rightAction}>Text</Text>;
}

type Item = {
  id: string;
  testID: string;
  title: string;
};

const generateItems = (count: number): Item[] => {
  return Array(count)
    .fill(0)
    .map((value, idx) => {
      // render speed test
      return {
        id: String(idx),
        testID: String(idx),
        title: `Number ${idx}`,
      };

      // re-renders test
      // return {
      //   id: String(idx),
      //   testID: String(value),
      //   title: `Number ${value}`,
      // };
    });
};

const data = generateItems(200);

const _RenderItemView = (item: Item) => (
  <View style={styles.swipeable}>
    <Text>{item.title}</Text>
  </View>
);

const RenderItemViewMemoed = memo(_RenderItemView);

export default function Example() {
  const renderItem: ListRenderItem<Item> = ({ item }) => {
    return (
      <ReanimatedSwipeable
        renderRightActions={RightAction}
        testID={item.testID}>
        <RenderItemViewMemoed {...item} />
      </ReanimatedSwipeable>
    );
  };

  const flashListRef = useRef<FlashList<Item> | null>(null);
  useBenchmark(flashListRef, (callback) => {
    Alert.alert('result', callback.formattedString);
  });

  return (
    <GestureHandlerRootView>
      <FlashList
        ref={flashListRef}
        data={data}
        renderItem={renderItem}
        estimatedItemSize={50}
        keyExtractor={(item) => item.id}
      />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  leftAction: { width: 50, height: 50, backgroundColor: 'crimson' },
  rightAction: { width: 50, height: 50, backgroundColor: 'purple' },
  separator: {
    width: '100%',
    borderTopWidth: 1,
  },
  swipeable: {
    height: 50,
    backgroundColor: 'papayawhip',
    alignItems: 'center',
  },
});

Does this issue still occur for you on version 2.22.0-rc.0? If so, could you please provide a reproduction where the difference is visible?

latekvo avatar Jan 09 '25 11:01 latekvo

I also encounter the slow performance on scrolling on ReanimatedSwipeable, it looks jittery when scrolled, but only when I use the methods like onSwipeableOpen or onSwipeableClose. When I remove these, it's back to normal.

<ReanimatedSwipeable //containerStyle={[style]} ref={reanimatedRef} friction={2} leftThreshold={80} rightThreshold={80} renderLeftActions={LeftAction} renderRightActions={RightAction} onSwipeableOpen={() => { console.log('jittery'); }} > {children} </ReanimatedSwipeable>

Can you please try and check this?

davelynInes avatar Jan 15 '25 07:01 davelynInes

I second the usage of onSwipeableOpen={() => { }} leads to huge stutter.

@davelynInes have you found any workaround for using onSwipeableOpen without having an unusable app ?

Baart avatar Feb 12 '25 14:02 Baart

I second the usage of onSwipeableOpen={() => { }} leads to huge stutter.

@davelynInes have you found any workaround for using onSwipeableOpen without having an unusable app ?

I used the old/legacy react-native-gesture-handler/Swipeable for now. I didn't have issues with it.

davelynInes avatar Feb 12 '25 22:02 davelynInes

I confirm the new Swipeable is very slow within an Animated.FlatList.

RohovDmytro avatar Feb 19 '25 13:02 RohovDmytro

I second the usage of onSwipeableOpen={() => { }} leads to huge stutter. @davelynInes have you found any workaround for using onSwipeableOpen without having an unusable app ?

I used the old/legacy react-native-gesture-handler/Swipeable for now. I didn't have issues with it.

What version are you using

zrina1314 avatar Mar 03 '25 13:03 zrina1314

I second the usage of onSwipeableOpen={() => { }} leads to huge stutter.

@davelynInes have you found any workaround for using onSwipeableOpen without having an unusable app ?

I used the old/legacy react-native-gesture-handler/Swipeable for now. I didn't have issues with it.

Same here... Old swipeable is so clean and fast in android

roo12312 avatar May 04 '25 18:05 roo12312

I added ReanimatedSwipeable to a SectionList and JS FPS dropped from 60 to much lower during scrolling and other animations.

I spent a few hours trying to figure out if there was something wrong with my code and optimize performance. Commenting out the wrapping <ReanimatedSwipeable> brought back buttery performance. It must be either creating a bunch of views, or running a bunch of code, because it's a serious drain on performance.

mysticmew7 avatar Sep 03 '25 04:09 mysticmew7

Can confirm that The drain in performance is is huge, almost doubling rendering time of each of my list item.

RohovDmytro avatar Sep 20 '25 13:09 RohovDmytro

Hi, I'm still facing the same issue: Expo 54, React-Native 0.81.5, new architecture. FPS drops a lot when scrolling.

pablogdcr avatar Oct 29 '25 11:10 pablogdcr

Just to try to bump this issue: I'm having the same problem. I tried what @davelynInes suggested about using the old/legacy react-native-gesture-handler/Swipeable and it worked.

I noticed that there are other PRs and/or issues related to this too, so it would be great if this issue could be priorised 🙏🏻 Have a good day.

demetrio avatar Nov 02 '25 23:11 demetrio

So @davelynInes post (at the top of the thread) sparked my attention.

What we know is that setting up Gesture is a very expensive operation, and it’s usually recommended to useMemo the gesture. ReanimatedSwipeable tries that, but after inspecting the code I can see that onSwipeableOpen and onSwipeableClose are directly passed as dependencies to dispatchEndEvents and dispatchImmediateEvents, which are dependencies for animateRow. Long story short, that simple prop causes the whole memoization chain to break, rendering every effort to memoize the Gesture useless. That’s the ugly and tricky part about useCallback and useMemo: a single mistake is enough.

You could try to pass a stable function to onSwipeableOpen with useCallback and see if that fixes the issue. If that doesn’t work, ReanimatedSwipeable itself should be wrapped with memo on top of that. But ReanimatedSwipeable needs fully stable props to prevent tapGesture from being re-created over and over again.

I did not verify my findings, but I am pretty sure that this could be the culprit.

One more hint: if used with FlatList, which uses no recycling, using ReanimatedSwipeable is not recommended, as it will re-create the item every time, and the gesture setup is definitely expensive. Use Flashlist, or better, LegendList

hirbod avatar Nov 08 '25 11:11 hirbod