Skip to content

Commit

Permalink
perf: reduce the amount of work done when rendering data
Browse files Browse the repository at this point in the history
Before, even if the limit of the number of render is set, it will render one more layer of
BaseLayout, which makes the performance can not be maximized, and now the optimization makes
BaseLayout will not render any more, even if the number of data is 1 million, it will only render
the specified amount of render. Performance has improved dramatically.

fix #352, fix #362, fix #258, fix #478
  • Loading branch information
dohooo committed Dec 26, 2023
1 parent 766e267 commit 9f3a3d6
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 108 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-onions-chew.md
@@ -0,0 +1,5 @@
---
'react-native-reanimated-carousel': patch
---

Reduce the amount of work done when rendering data.
36 changes: 3 additions & 33 deletions src/components/BaseLayout.tsx
Expand Up @@ -2,15 +2,10 @@ import React from "react";
import type { ViewStyle } from "react-native";
import type { AnimatedStyleProp } from "react-native-reanimated";
import Animated, {
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useDerivedValue,
} from "react-native-reanimated";

import { LazyView } from "./LazyView";

import { useCheckMounted } from "../hooks/useCheckMounted";
import type { IOpts } from "../hooks/useOffsetX";
import { useOffsetX } from "../hooks/useOffsetX";
import type { IVisibleRanges } from "../hooks/useVisibleRanges";
Expand All @@ -28,7 +23,6 @@ export const BaseLayout: React.FC<{
animationValue: Animated.SharedValue<number>
}) => React.ReactElement
}> = (props) => {
const mounted = useCheckMounted();
const { handlerOffset, index, children, visibleRanges, animationStyle }
= props;

Expand All @@ -46,7 +40,7 @@ export const BaseLayout: React.FC<{
},
} = context;
const size = vertical ? height : width;
const [shouldUpdate, setShouldUpdate] = React.useState(false);

let offsetXConfig: IOpts = {
handlerOffset,
index,
Expand Down Expand Up @@ -79,28 +73,6 @@ export const BaseLayout: React.FC<{
[animationStyle],
);

const updateView = React.useCallback(
(negativeRange: number[], positiveRange: number[]) => {
mounted.current
&& setShouldUpdate(
(index >= negativeRange[0] && index <= negativeRange[1])
|| (index >= positiveRange[0] && index <= positiveRange[1]),
);
},
[index, mounted],
);

useAnimatedReaction(
() => visibleRanges.value,
() => {
runOnJS(updateView)(
visibleRanges.value.negativeRange,
visibleRanges.value.positiveRange,
);
},
[visibleRanges.value],
);

return (
<Animated.View
style={[
Expand All @@ -116,11 +88,9 @@ export const BaseLayout: React.FC<{
* e.g.
* The testID of first item will be changed to __CAROUSEL_ITEM_0_READY__ from __CAROUSEL_ITEM_0_NOT_READY__ when the item is ready.
* */
testID={`__CAROUSEL_ITEM_${index}_${shouldUpdate ? "READY" : "NOT_READY"}__`}
testID={`__CAROUSEL_ITEM_${index}__`}
>
<LazyView shouldUpdate={shouldUpdate}>
{children({ animationValue })}
</LazyView>
{children({ animationValue })}
</Animated.View>
);
};
67 changes: 15 additions & 52 deletions src/components/Carousel.tsx
Expand Up @@ -3,7 +3,7 @@ import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { runOnJS, useDerivedValue } from "react-native-reanimated";

import { BaseLayout } from "./BaseLayout";
import { ItemRenderer } from "./ItemRenderer";
import { ScrollViewGesture } from "./ScrollViewGesture";

import { useAutoPlay } from "../hooks/useAutoPlay";
Expand All @@ -13,7 +13,6 @@ import { useInitProps } from "../hooks/useInitProps";
import { useLayoutConfig } from "../hooks/useLayoutConfig";
import { useOnProgressChange } from "../hooks/useOnProgressChange";
import { usePropsErrorBoundary } from "../hooks/usePropsErrorBoundary";
import { useVisibleRanges } from "../hooks/useVisibleRanges";
import { CTX } from "../store";
import type { ICarouselInstance, TCarouselProps } from "../types";
import { computedRealIndexWithAutoFillData } from "../utils/computed-with-auto-fill-data";
Expand All @@ -30,8 +29,6 @@ const Carousel = React.forwardRef<ICarouselInstance, TCarouselProps<any>>(
data,
// Length of fill data
dataLength,
// Raw data that has not been processed
rawData,
// Length of raw data
rawDataLength,
mode,
Expand Down Expand Up @@ -155,55 +152,8 @@ const Carousel = React.forwardRef<ICarouselInstance, TCarouselProps<any>>(
[getCurrentIndex, next, prev, scrollTo],
);

const visibleRanges = useVisibleRanges({
total: dataLength,
viewSize: size,
translation: handlerOffset,
windowSize,
loop,
});

const layoutConfig = useLayoutConfig({ ...props, size });

const renderLayout = React.useCallback(
(item: any, i: number) => {
const realIndex = computedRealIndexWithAutoFillData({
index: i,
dataLength: rawDataLength,
loop,
autoFillData,
});

return (
<BaseLayout
key={i}
index={i}
handlerOffset={offsetX}
visibleRanges={visibleRanges}
animationStyle={customAnimation || layoutConfig}
>
{({ animationValue }) =>
renderItem({
item,
index: realIndex,
animationValue,
})
}
</BaseLayout>
);
},
[
loop,
rawData,
offsetX,
visibleRanges,
autoFillData,
renderItem,
layoutConfig,
customAnimation,
],
);

return (
<GestureHandlerRootView>
<CTX.Provider value={{ props, common: commonVariables }}>
Expand All @@ -228,7 +178,20 @@ const Carousel = React.forwardRef<ICarouselInstance, TCarouselProps<any>>(
onTouchBegin={scrollViewGestureOnTouchBegin}
onTouchEnd={scrollViewGestureOnTouchEnd}
>
{data.map(renderLayout)}
<ItemRenderer
data={data}
dataLength={dataLength}
rawDataLength={rawDataLength}
loop={loop}
size={size}
windowSize={windowSize}
autoFillData={autoFillData}
offsetX={offsetX}
handlerOffset={handlerOffset}
layoutConfig={layoutConfig}
renderItem={renderItem}
customAnimation={customAnimation}
/>
</ScrollViewGesture>
</CTX.Provider>
</GestureHandlerRootView>
Expand Down
105 changes: 105 additions & 0 deletions src/components/ItemRenderer.tsx
@@ -0,0 +1,105 @@
import React from "react";
import type { FC } from "react";
import type { ViewStyle } from "react-native";
import type Animated from "react-native-reanimated";
import { useAnimatedReaction, type AnimatedStyleProp, runOnJS } from "react-native-reanimated";

import type { TAnimationStyle } from "./BaseLayout";
import { BaseLayout } from "./BaseLayout";

import type { VisibleRanges } from "../hooks/useVisibleRanges";
import { useVisibleRanges } from "../hooks/useVisibleRanges";
import type { CarouselRenderItem } from "../types";
import { computedRealIndexWithAutoFillData } from "../utils/computed-with-auto-fill-data";

interface Props {
data: any[]
dataLength: number
rawDataLength: number
loop: boolean
size: number
windowSize?: number
autoFillData: boolean
offsetX: Animated.SharedValue<number>
handlerOffset: Animated.SharedValue<number>
layoutConfig: TAnimationStyle
renderItem: CarouselRenderItem<any>
customAnimation?: ((value: number) => AnimatedStyleProp<ViewStyle>)
}

export const ItemRenderer: FC<Props> = (props) => {
const {
data,
size,
windowSize,
handlerOffset,
offsetX,
dataLength,
rawDataLength,
loop,
autoFillData,
layoutConfig,
renderItem,
customAnimation,
} = props;

const visibleRanges = useVisibleRanges({
total: dataLength,
viewSize: size,
translation: handlerOffset,
windowSize,
loop,
});

const [displayedItems, setDisplayedItems] = React.useState<VisibleRanges>(null!);

useAnimatedReaction(
() => visibleRanges.value,
ranges => runOnJS(setDisplayedItems)(ranges),
[visibleRanges],
);

if (!displayedItems)
return null;

return (
<>
{
data.map((item, index) => {
const realIndex = computedRealIndexWithAutoFillData({
index,
dataLength: rawDataLength,
loop,
autoFillData,
});

const { negativeRange, positiveRange } = displayedItems;

const shouldRender = (index >= negativeRange[0] && index <= negativeRange[1])
|| (index >= positiveRange[0] && index <= positiveRange[1]);

if (!shouldRender)
return null;

return (
<BaseLayout
key={index}
index={index}
handlerOffset={offsetX}
visibleRanges={visibleRanges}
animationStyle={customAnimation || layoutConfig}
>
{({ animationValue }) =>
renderItem({
item,
index: realIndex,
animationValue,
})
}
</BaseLayout>
);
})
}
</>
);
};
2 changes: 1 addition & 1 deletion src/hooks/useOffsetX.test.ts
Expand Up @@ -12,7 +12,7 @@ describe("useSharedValue", () => {
const range = useSharedValue({
negativeRange: [7, 9],
positiveRange: [0, 3],
});
}) as IVisibleRanges;
const inputs: Array<{
config: IOpts
range: IVisibleRanges
Expand Down

0 comments on commit 9f3a3d6

Please sign in to comment.