hyochan / react-native-masonry-list

The Masonry List implementation which has similar implementation as the `FlatList` in React Native
MIT License
393 stars 55 forks source link

how we can handle performance in case of large datasets ?? i mean virtualization #8

Closed soly2014 closed 3 years ago

soly2014 commented 3 years ago

Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

Describe the solution you'd like A clear and concise description of what you want to happen.

Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered.

Additional context Add any other context or screenshots about the feature request here.

hyochan commented 3 years ago

image

Will this help? I don't think I fully understand your intention. The screenshot posted is from youtube.

soly2014 commented 3 years ago

@hyochan what i mean is using Flatlist like this lib https://github.com/iftechio/react-native-virtualized-masonry

paulrostorp commented 2 years ago

I think the point made by @soly2014 is a valid discussion. This issue was closed prematurely @hyochan

The documentation states the following:

Pinterest like listview made in React Native. It just behaves like the FlatList so it is easy to use.

I do not believe that statement to be entirely true: This module may have an api similar to that of a FlatList, but fundamentally, it behaves like a scrollview.

From the react native documentation: Screenshot 2021-12-31 at 10 43 08

Although this module may allow for infinite scrolling, as you keep scrolling, your data list will grow and eventually use up a lot of memory, especially if you are rendering images...

Supporting "windowing" could be a solution without needing to refactor the whole library. Users could then do something like:

<MasonryList
    data={data.slice(offset, offset + 10)}
    {...}
    onEndReached={(): void => {
    setOffset(offset + 10);
    }}
  />

Unfortunately this is not supported at this time.

j-cheung commented 2 years ago

hi all, i was experiencing some heavy lag on Android when using the MasonryList with a large dataset, or when there are multiple lists that were rendered (some on out of focus screens), and was looking for a virtualized version, but have not found one that fits my use.

as a result, i've made some changes to this component, shown in the following patch.

more of a work in progress, only works for vertical atm (only using vertical in my app right now)

thanks!

bit of an explanation:

diff --git a/node_modules/@react-native-seoul/masonry-list/lib/index.d.ts b/node_modules/@react-native-seoul/masonry-list/lib/index.d.ts
index 3458b95..effa73b 100644
--- a/node_modules/@react-native-seoul/masonry-list/lib/index.d.ts
+++ b/node_modules/@react-native-seoul/masonry-list/lib/index.d.ts
@@ -22,6 +22,9 @@ interface Props<T> extends Omit<ScrollViewProps, 'refreshControl'> {
     containerStyle?: StyleProp<ViewStyle>;
     numColumns?: number;
     keyExtractor?: ((item: T | any, index: number) => string) | undefined;
+    refreshOffsetInterval?: number;
+    windowOffset?: number; // to workaround empty space rendered when scrolling up
+    gutterWidth?: number; // width between columns ... only for vertical currently
 }
 declare function MasonryList<T>(props: Props<T>): ReactElement;
 declare const _default: React.MemoExoticComponent<typeof MasonryList>;
diff --git a/node_modules/@react-native-seoul/masonry-list/lib/index.js b/node_modules/@react-native-seoul/masonry-list/lib/index.js
index a70f495..f8f0f6a 100644
--- a/node_modules/@react-native-seoul/masonry-list/lib/index.js
+++ b/node_modules/@react-native-seoul/masonry-list/lib/index.js
@@ -9,8 +9,8 @@ var __rest = (this && this.__rest) || function (s, e) {
         }
     return t;
 };
-import { RefreshControl, ScrollView, View, } from 'react-native';
-import React, { memo, useState } from 'react';
+import { RefreshControl, ScrollView, View, Dimensions} from 'react-native';
+import React, { memo, useState, useRef, useEffect } from 'react';
 const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }, onEndReachedThreshold) => {
     const paddingToBottom = contentSize.height * onEndReachedThreshold;
     return (layoutMeasurement.height + contentOffset.y >=
@@ -18,14 +18,60 @@ const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }, onEn
 };
 function MasonryList(props) {
     const [isRefreshing, setIsRefreshing] = useState(false);
-    const { refreshing, data, innerRef, ListHeaderComponent, ListEmptyComponent, ListFooterComponent, ListHeaderComponentStyle, containerStyle, contentContainerStyle, renderItem, onEndReachedThreshold, onEndReached, onRefresh, loading, LoadingView, numColumns = 2, horizontal, onScroll, removeClippedSubviews = false, keyExtractor, } = props;
+    const { refreshing, data, innerRef, ListHeaderComponent, ListEmptyComponent, ListFooterComponent, ListHeaderComponentStyle, containerStyle, contentContainerStyle, renderItem, onEndReachedThreshold, onEndReached, onRefresh, loading, LoadingView, numColumns = 2, horizontal, onScroll, removeClippedSubviews = false, keyExtractor, refreshOffsetInterval=500, windowOffset=0, gutterWidth=0 } = props;
     const { style } = props, propsWithoutStyle = __rest(props, ["style"]);
+    const offset = useRef(0);
+    const [refreshOffset, setRefreshOffset] = useState(0);
+    const items = useRef({});
+
+    const _renderItem = (col, i, el) => {
+        const outOfView =
+            ((items?.current?.[col]?.[i]?.offset ?? 99999) <
+            (offset?.current ?? 0) - Dimensions.get("window").height  - windowOffset)
+            ||
+            ((items?.current?.[col]?.[i]?.offset ?? 0) >
+            (offset?.current ?? 0) + Dimensions.get("window").height)
+        if (outOfView) {
+            return (
+            <View style={{ height: items?.current?.[col]?.[i]?.height ?? 100 }} key={`masonry-empty-spacer-${col}-${i}`}/>
+            );
+        }
+        return (
+            <View
+            key={keyExtractor === null || keyExtractor === void 0 ? void 0 : keyExtractor(el, i)}
+            onLayout={({ nativeEvent }) => {
+                const itemsCopy = items?.current ?? {};
+                const itemsColumn = itemsCopy[col] ?? {};
+                itemsColumn[i] = {
+                offset: nativeEvent?.layout?.y ?? 0,
+                height: nativeEvent?.layout?.height ?? 0,
+                };
+                items.current = { ...items?.current, [col]: itemsColumn };
+            }}
+            >
+            {renderItem({ item: el, i })}
+            </View>
+        );
+    };
+
+    useEffect(() => {
+    return () => {
+        offset.current = 0;
+        items.current = {};
+    };
+    }, []);
+
     return (<ScrollView {...propsWithoutStyle} ref={innerRef} style={[{ flex: 1, alignSelf: 'stretch' }, containerStyle]} contentContainerStyle={contentContainerStyle} removeClippedSubviews={removeClippedSubviews} refreshControl={<RefreshControl refreshing={!!(refreshing || isRefreshing)} onRefresh={() => {
                 setIsRefreshing(true);
                 onRefresh === null || onRefresh === void 0 ? void 0 : onRefresh();
                 setIsRefreshing(false);
             }}/>} scrollEventThrottle={16} onScroll={(e) => {
             const nativeEvent = e.nativeEvent;
+            const offsetCurrent = nativeEvent?.contentOffset?.y ?? 0;
+            offset.current = offsetCurrent;
+            if (Math.abs(offsetCurrent - refreshOffset) > refreshOffsetInterval) {
+            setRefreshOffset(offsetCurrent);
+            }
             if (isCloseToBottom(nativeEvent, onEndReachedThreshold || 0.1))
                 onEndReached === null || onEndReached === void 0 ? void 0 : onEndReached();
             onScroll === null || onScroll === void 0 ? void 0 : onScroll(e);
@@ -42,13 +88,16 @@ function MasonryList(props) {
                 return (<View key={`masonry-column-${num}`} style={{
                         flex: 1 / numColumns,
                         flexDirection: horizontal ? 'row' : 'column',
+                        paddingRight:(num < numColumns-1) ? gutterWidth : 0
+                        // paddingRight:16
                     }}>
                 {data
                         .map((el, i) => {
                         if (i % numColumns === num)
-                            return (<View key={keyExtractor === null || keyExtractor === void 0 ? void 0 : keyExtractor(el, i)}>
-                          {renderItem({ item: el, i })}
-                        </View>);
+                            // return (<View key={keyExtractor === null || keyExtractor === void 0 ? void 0 : keyExtractor(el, i)}>
+                          return _renderItem(num, i, el)
+                          {/* {renderItem({ item: el, i })} */}
+                        // </View>);
                         return null;
                     })
                         .filter((e) => !!e)}
iarmankhan commented 2 years ago

Hey @j-cheung Better would be if you can fork out the library and provide this patch. I think this is a much needed thing if we have to maintain performance

j-cheung commented 2 years ago

Hey @j-cheung Better would be if you can fork out the library and provide this patch. I think this is a much needed thing if we have to maintain performance

hi @iarmankhan, i'll look into it when i'm free, feel free to do it as well. i only did the patch because i was mostly focused on trying to get the change on my production app asap. thanks.