From 9f3a3d6be31a251528fce89613b6308c5382fa93 Mon Sep 17 00:00:00 2001 From: Caspian Date: Tue, 26 Dec 2023 02:00:22 +0000 Subject: [PATCH] perf: reduce the amount of work done when rendering data 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 --- .changeset/gold-onions-chew.md | 5 ++ src/components/BaseLayout.tsx | 36 +---------- src/components/Carousel.tsx | 67 +++++--------------- src/components/ItemRenderer.tsx | 105 ++++++++++++++++++++++++++++++++ src/hooks/useOffsetX.test.ts | 2 +- src/hooks/useVisibleRanges.tsx | 76 ++++++++++++++++------- 6 files changed, 183 insertions(+), 108 deletions(-) create mode 100644 .changeset/gold-onions-chew.md create mode 100644 src/components/ItemRenderer.tsx diff --git a/.changeset/gold-onions-chew.md b/.changeset/gold-onions-chew.md new file mode 100644 index 00000000..48d0ef19 --- /dev/null +++ b/.changeset/gold-onions-chew.md @@ -0,0 +1,5 @@ +--- +'react-native-reanimated-carousel': patch +--- + +Reduce the amount of work done when rendering data. diff --git a/src/components/BaseLayout.tsx b/src/components/BaseLayout.tsx index 0b8ae7d8..b158dc35 100644 --- a/src/components/BaseLayout.tsx +++ b/src/components/BaseLayout.tsx @@ -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"; @@ -28,7 +23,6 @@ export const BaseLayout: React.FC<{ animationValue: Animated.SharedValue }) => React.ReactElement }> = (props) => { - const mounted = useCheckMounted(); const { handlerOffset, index, children, visibleRanges, animationStyle } = props; @@ -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, @@ -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 ( - - {children({ animationValue })} - + {children({ animationValue })} ); }; diff --git a/src/components/Carousel.tsx b/src/components/Carousel.tsx index 11a5e377..2e10ef23 100644 --- a/src/components/Carousel.tsx +++ b/src/components/Carousel.tsx @@ -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"; @@ -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"; @@ -30,8 +29,6 @@ const Carousel = React.forwardRef>( data, // Length of fill data dataLength, - // Raw data that has not been processed - rawData, // Length of raw data rawDataLength, mode, @@ -155,55 +152,8 @@ const Carousel = React.forwardRef>( [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 ( - - {({ animationValue }) => - renderItem({ - item, - index: realIndex, - animationValue, - }) - } - - ); - }, - [ - loop, - rawData, - offsetX, - visibleRanges, - autoFillData, - renderItem, - layoutConfig, - customAnimation, - ], - ); - return ( @@ -228,7 +178,20 @@ const Carousel = React.forwardRef>( onTouchBegin={scrollViewGestureOnTouchBegin} onTouchEnd={scrollViewGestureOnTouchEnd} > - {data.map(renderLayout)} + diff --git a/src/components/ItemRenderer.tsx b/src/components/ItemRenderer.tsx new file mode 100644 index 00000000..0227bbb2 --- /dev/null +++ b/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 + handlerOffset: Animated.SharedValue + layoutConfig: TAnimationStyle + renderItem: CarouselRenderItem + customAnimation?: ((value: number) => AnimatedStyleProp) +} + +export const ItemRenderer: FC = (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(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 ( + + {({ animationValue }) => + renderItem({ + item, + index: realIndex, + animationValue, + }) + } + + ); + }) + } + + ); +}; diff --git a/src/hooks/useOffsetX.test.ts b/src/hooks/useOffsetX.test.ts index 86360d12..85d3757a 100644 --- a/src/hooks/useOffsetX.test.ts +++ b/src/hooks/useOffsetX.test.ts @@ -12,7 +12,7 @@ describe("useSharedValue", () => { const range = useSharedValue({ negativeRange: [7, 9], positiveRange: [0, 3], - }); + }) as IVisibleRanges; const inputs: Array<{ config: IOpts range: IVisibleRanges diff --git a/src/hooks/useVisibleRanges.tsx b/src/hooks/useVisibleRanges.tsx index 76364669..674c0023 100644 --- a/src/hooks/useVisibleRanges.tsx +++ b/src/hooks/useVisibleRanges.tsx @@ -1,10 +1,15 @@ +import { useRef } from "react"; import type Animated from "react-native-reanimated"; import { useDerivedValue } from "react-native-reanimated"; -export type IVisibleRanges = Animated.SharedValue<{ - negativeRange: number[] - positiveRange: number[] -}>; +type Range = [number, number]; + +export interface VisibleRanges { + negativeRange: Range + positiveRange: Range +} + +export type IVisibleRanges = Animated.SharedValue; export function useVisibleRanges(options: { total: number @@ -22,6 +27,7 @@ export function useVisibleRanges(options: { } = options; const windowSize = _windowSize ?? total; + const cachedRanges = useRef(null!); const ranges = useDerivedValue(() => { const positiveCount = Math.round(windowSize / 2); @@ -30,36 +36,62 @@ export function useVisibleRanges(options: { let currentIndex = Math.round(-translation.value / viewSize); currentIndex = currentIndex < 0 ? (currentIndex % total) + total : currentIndex; + let newRanges: VisibleRanges; + if (!loop) { // Adjusting negative range if the carousel is not loopable. // So, It will be only displayed the positive items. - return { + newRanges = { negativeRange: [0 + currentIndex - (windowSize - 1), 0 + currentIndex], positiveRange: [0 + currentIndex, currentIndex + (windowSize - 1)], }; } + else { + const negativeRange: Range = [ + (currentIndex - negativeCount + total) % total, + (currentIndex - 1 + total) % total, + ]; - const negativeRange = [ - (currentIndex - negativeCount + total) % total, - (currentIndex - 1 + total) % total, - ]; + const positiveRange: Range = [ + (currentIndex + total) % total, + (currentIndex + positiveCount + total) % total, + ]; - const positiveRange = [ - (currentIndex + total) % total, - (currentIndex + positiveCount + total) % total, - ]; + if (negativeRange[0] < total && negativeRange[0] > negativeRange[1]) { + negativeRange[1] = total - 1; + positiveRange[0] = 0; + } + if (positiveRange[0] > positiveRange[1]) { + negativeRange[1] = total - 1; + positiveRange[0] = 0; + } - if (negativeRange[0] < total && negativeRange[0] > negativeRange[1]) { - negativeRange[1] = total - 1; - positiveRange[0] = 0; - } - if (positiveRange[0] > positiveRange[1]) { - negativeRange[1] = total - 1; - positiveRange[0] = 0; + // console.log({ negativeRange, positiveRange ,total,windowSize,a:total <= _windowSize}) + newRanges = { negativeRange, positiveRange }; } - // console.log({ negativeRange, positiveRange ,total,windowSize,a:total <= _windowSize}) - return { negativeRange, positiveRange }; + + if ( + isArraysEqual( + cachedRanges.current?.negativeRange ?? [], + newRanges.negativeRange, + ) + && isArraysEqual( + cachedRanges.current?.positiveRange ?? [], + newRanges.positiveRange, + ) + ) + return cachedRanges.current; + + cachedRanges.current = newRanges; + return cachedRanges.current; }, [loop, total, windowSize, translation]); return ranges; } + +function isArraysEqual(a: number[], b: number[]): boolean { + "worklet"; + if (a.length !== b.length) return false; + + return a.every((value, index) => value === b[index]); +}