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

Optimise `ReanimatedSwipeable` component

Open latekvo opened this issue 1 year ago • 1 comments

Description

  • Memoize all Gestures, and non-memoized functions.
  • Try reduce number of used shared values

Test plan

  • use android, iOS has no performance issues
  • use attached code to measure performance
    • tested on physical Xiaomi Redmi Note 8T, specs: Octa-core CPU 2x4 cores @ 2GHz
    • pre-optimization average: 1±0.3 fps
    • post-optimization average: 3.2±1.5 fps

Test code

Tested on @shopify/flash-list using their useBenchmark utility hook.

Collapsed code
import { 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';

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

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

const generateItems = (count: number): Item[] => {
  return Array.from({ length: count }, () => {
    const randomNumber = Math.random().toString(36).substring(2, 8);
    return {
      id: randomNumber,
      title: `Title ${randomNumber}`,
    };
  });
};

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}>
        <RenderItemViewMemoed {...item} />
      </ReanimatedSwipeable>
    );
  };

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

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

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',
  },
});

latekvo avatar Oct 17 '24 11:10 latekvo

Performance tests.

format: fps | changes in relation to the previous run < comment

tested on: Xiaomi Redmi Note 8T:

1.0 | baseline
1.6 | remove all returned elements but the view with children nested
2.0 | remove animated styles < less stutters
2.6 | remove all unused variables
4.6 | remove all unused variables < large leap
4.8 | remove all unused variables
6.2 | delete unused imports < large leap + less stutters
6.5 | stop changing methods ref every render < done in 104761f
10.7 | remove all remaining unused declarations < large improvement
11.7 | remove everything from render besides Animated.View with kids inside
11.3 | remove everything including types, cleanup, leave just RN View with children
11.9 | remove forward ref wrapping everything
11.7 | nothing remains except view with styles

tested on: Xiaomi POCO X3 Pro:

6.3 | baseline
8.5 | remove all returned components except for animated.view with styles and children
10.4 | remove animated styles from animated.view < loads instantly instead of ~1s
11.1 | remove all unused variables
21.4 | remove all unused variables < same large leap as Redmi Note 8T
- 10.9 | revert
- 15.6 | remove updateAnimatedEvent callback < 43% improvement 
- 19.6 | remove handleRelease callback < 25% improvement
50.9 | remove all remaining unused things until none remain
50.2 | nothing remains except view with styles < to see top possible performance

Based on these tests, I believe no improvement past 2x is realistic, which still likely won't be satisfactory. Majority of the issues are unavoidable.

Note:

There are discrepancies between the baseline here, and the baseline listed in the Test Plan. These tests were performed around 5 days apart. I'm not entirely sure why these discrepancies occur, reverting to older commits did not seem to revert to better performance, regardless of that, the relative distances between the values listed in these tests are consistant between different devices, so there isn't a point in redoing them to match current device performance.

latekvo avatar Oct 18 '24 11:10 latekvo

I don't understand this part:

2.6 | remove all unused variables 4.6 | remove all unused variables < large leap 4.8 | remove all unused variables

How did you manage to remove all unused variables, and then do this again and again?

m-bert avatar Oct 25 '24 07:10 m-bert

How did you manage to remove all unused variables, and then do this again and again?

Upon removing unused values, new, previously used values become unused. By removing unused values via VSC's remove all unused declarations functionality, same heaps of unused declarations can be consistently removed. This helps with sifting out which variables/functions are associated with the largest fps changes.

As can be seen, the second remove all unused variables step causes the largest fps improvement among the early steps, this is due to removal of updateAnimatedEvent and handleRelease callbacks (43% and 25% improvement respectively), unfortunately neither of these functions had any glaring issues, and i don't think much can be done to optimise them.

latekvo avatar Oct 25 '24 09:10 latekvo

Upon removing unused values, new, previously used values become unused.

Yes, I get that. The thing is, in such report I'd expect only one entry about removing unused variables, in which whole chain is removed, not just first layer 😅

m-bert avatar Oct 25 '24 10:10 m-bert

@latekvo Thanks for the PR and for fixing this issue!

pranavbabu avatar Nov 08 '24 08:11 pranavbabu