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

Slow FlatList render when list item contains Swipeable

Open davidcarboni opened this issue 10 months ago • 7 comments

Description

I'm seeing a significant slowdown in rendering when I add a Swipeable to the items in a FlatList.

This visually looks about 2x slower on my Androids (emulator, old device, newer device). iOS is less noticeable (iPhone 11) but from what I've profiled it looks like iOS is also slowing down.

By eye it looks like my screen takes ~2s to render on my newer device (~3s on the older device/emulator) with Swipeable present but ~0.5s (~1s on the older device/emulator) when I delete the Swipeable.

I've tried to create a Snack, but it seems to be using Expo 51 and I'm seeing this in Expo 52 with New Architecture. I've still given the link to the Snack below just in case this is helpful.

Steps to reproduce

I wanted to provide as useful an issue as I can in the hope this is helpful, so I've done my best to optimise and strip down my code (see below) and then profiled performance when the Swipeable component is present and, for comparison, when I delete it.

  1. Add swipeable to a list item in FlatList (I think other similar lists too)
  2. Render performance is significantly slower
  3. Remove Swipeable (everything else unchanged) and performance increases noticeably

I've optimised what I can in my code (memo and useCallback) and stripped it back in case it's something else I'm doing. I'm including the code below.

I then used the profiler to see where the slowdown is coming from. Swipeable seems to be taking a chunk of time on each item. I'm including screenshots and exported profile data. I've highlighted the VirtualizedList component with a red dot in the screenshots for visual comparison. The trace highlihts Swipeable in yellow for items rendered.

Performance profiles:

With Swipeable

Image

swipeable-profiling-data.17-01-2025.22-03-28.json

Without Swipeable

Image

no-swipeable-profiling-data.17-01-2025.22-08-42.json

Code

Stripped back as much as possible to make it easier to reproduce if needed.

The Swipeable is the outermost component of the SearchResult list item. Deleting the Swipeable significantly reduces rendering time, particularly noticeable on Android, especially an older real device I'm testing with:

import { memo, ReactElement, useCallback } from 'react';
import { StyleSheet, View, FlatList, Pressable, Text } from 'react-native';
import { Image } from 'expo-image';
import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable';

const deleteIcon = require('../assets/icons/trash.svg');

export interface Client {
  id: string,
  firstName: string,
  lastName?: string,
}

interface SearchResultProps {
  client: Client,
  onSelect: (client: Client) => void,
  renderRightActions: (client: Client) => ReactElement,
}

interface ClientListProps {
  clients: Client[];
  onDelete: (id: string) => void,
}

/**
 * List item
 */
export const SearchResult = memo(({
  client,
  onSelect,
  renderRightActions,
}: SearchResultProps) => {
  return (
    // This Swipeable seems to cause the issue
    <Swipeable renderRightActions={() => renderRightActions(client)} containerStyle={{ borderRadius: 999, backgroundColor: 'gray', height: 48, marginBottom: 12 }}>
      <View style={styles.searchResult}>
        <Pressable onPress={() => onSelect(client)}>
          <View style={styles.searchResultRow}>
            <Text style={styles.searchResultText}>{client.firstName} {client.lastName}</Text>
          </View>
        </Pressable>
      </View>
    </Swipeable>
  );
}, (prevProps, nextProps) => {
  return (
    prevProps.client.firstName === nextProps.client.firstName &&
    prevProps.client.lastName === nextProps.client.lastName
  );
});

/**
 * FlatList
 */
export default function ClientList({ clients, onDelete }: ClientListProps): ReactElement {

  // Create a render function for the left swipe action (delete)
  const renderRightActions = useCallback((client: Client) => (
    <Pressable onPress={() => { onDelete(client.id); }} style={styles.actionButton}>
      <View style={styles.actionIconContainer}>
        <Image source={deleteIcon} style={[styles.actionIcon, { tintColor: 'white' }]} contentFit="contain" testID="deleteIcon" />
      </View>
    </Pressable>
  ), []);

  const renderitem = useCallback(({ item }: { item: Client, index: number; }) => (
    <SearchResult client={item} onSelect={() => { }} renderRightActions={renderRightActions} />
  ), []);

  return (
    <View style={{ flex: 1 }}>
      <View style={styles.searchResultsContainer}>
        <FlatList
          data={clients}
          keyExtractor={(i) => i.id}
          renderItem={renderitem}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  searchResultsContainer: {
    flex: 4,
    backgroundColor: 'white',
  },
  actionButton: {
    width: 48,
    height: 48,
    borderRadius: 999,
    backgroundColor: 'red',
  },
  actionIconContainer: {
    height: '100%',
    width: '100%',
    justifyContent: 'center',
    alignItems: 'center',
  },
  actionIcon: {
    height: 28,
    width: 28,
  },
  searchResult: {
    marginBottom: 12, // Make up for 'gap' not working in FlatList
  },
  searchResultRow: {
    height: 48,
    flexDirection: 'row',
    gap: 16,
    alignItems: 'center',
    backgroundColor: 'white',
    borderRadius: 999,
  },
  searchResultText: {
    fontSize: 16,
    lineHeight: 24,
    color: 'black',
  },
});

"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1"

Snack or a link to a repository

https://snack.expo.dev/@davidcarboni/swipeable-performance

Gesture Handler version

~2.20.2

React Native version

0.76.6

Platforms

Android, iOS

JavaScript runtime

Hermes

Workflow

Expo managed workflow

Architecture

Fabric (New Architecture)

Build type

Debug mode

Device

Real device

Device model

OnePlus 9, OnePlus 5 (also Android Emulator and iPhone 11)

Acknowledgements

Yes

davidcarboni avatar Jan 17 '25 23:01 davidcarboni

Hi! Thank you for reporting this issue and for detailed description. However, this seems to be a duplicate of #3307 and #3141. Let me know if that's not the case.

m-bert avatar Jan 20 '25 15:01 m-bert

I looked into this issue, this seems to be a separate problem than both https://github.com/software-mansion/react-native-gesture-handler/issues/3307 and https://github.com/software-mansion/react-native-gesture-handler/issues/3141.

It seems the very first Swipeable component takes around 300x longer to render on Android than it does on Web, every following Swipeable takes 3x longer than it does on Web (both values already adjusted for device performance by the factor of 3x).

I'll look into this issue more.

latekvo avatar Jan 20 '25 17:01 latekvo

Thanks for taking a look, much appreciated.

Thank you also the pointers to https://github.com/software-mansion/react-native-gesture-handler/issues/3307 and https://github.com/software-mansion/react-native-gesture-handler/issues/3141, I've had a look at those two issues and @latekvo may be right in saying it's different. If I undrestand correctly, in these issues it's the rendering when scrolling, rather than initial load, which seems to be highlighted?

I guess scrolling also causes items to render so it may still be the same issue (my list doesn't have enough items to extend outside the window). Looking at the videos in https://github.com/software-mansion/react-native-gesture-handler/issues/3141 if feels like the initial render is smooth (which is not the case for me) and the issue arises when scrolling outside the window.

davidcarboni avatar Jan 20 '25 21:01 davidcarboni

In my case it's the initial load and unmounting that takes at least x3 times more time with Swipeable component. Old arch, latest versions of libs.

RohovDmytro avatar Jan 29 '25 15:01 RohovDmytro

Hey, we've looked into this issue and narrowed down the majority of this issue to Animated.View, which takes around 68% of the loading time (82 out of 121ms). Replacing it with regular View completely removes this excess loading time. Unfortunately, we cannot just remove it, because Animated.View plays an important role in the Swipeable.

We'll take a look at the remaining 32% separately, but as of now, the Swipeable has already been heavily optimised, and so I'm not sure how much more performance we will be able to get out of it.

I've forwarded the Animated.View issue to the react-native-reanimated repo: https://github.com/software-mansion/react-native-reanimated/issues/6955

latekvo avatar Jan 29 '25 18:01 latekvo

When in a FlatList, using onSwipeableOpen={() => { }} tends to brutally decrease performance even with an empty call.

Baart avatar Feb 12 '25 14:02 Baart

I have same issue on android with react-native-gesture-handler: "0.26.0" and "react-native-reanimated": "3.18.0" When I use FlatList from react-native-gesture-handler - list scroll is glitchy. I need react-native-gesture-handler for SwipableMenu for every item of list. But when I switch to FlatList from React Native itself - all cool and sharp. List scrolling became smooth again. And essentially I have to remove swipe interaction in that case. What the reason? I want to use swipe gesture for displaying favorite button. Replacing it by long press will looks non naturally for users.

dehimer avatar Jun 21 '25 15:06 dehimer