react-nativeperformancereact-hookscamera-roll

How can I improve performance of this React Native code?


I am working on React Native app of mine and I have a screen where I am using RN Cameraroll and getting all of the images and allow user to select n number of images. I am marking images that are selected by user and marking them in order. When I was writing code of the screen, I used Android emulator and it worked just fine. But when I tested it in my Samsung s7 device (one of old gen phone that I have as test device) it didn't work as expected. So the view gets updated and it renders conditional view where it blurs out image and displays that image's number in order. Here is gif file of it,

image

As you can see, it is rendering numbers and all but it is slow. Now this works slowly in this device specially. I have couple other Android devices that I have tested this screen but they are newer devices and faster devices so they work better. Not smooth as my emulator but they work okay. So there is performance issue but I guess if I know how to fix it for this device, all devices performance will get fixed.

I am using useState() hook to store order of images path in array. so here is my code,


const SCREEN_WIDTH = Math.floor(Dimensions.get('window').width);
const PADDING = Math.floor(SCREEN_WIDTH * 0.015);
const RENDER_IMAGES_PER_ROW = SCREEN_WIDTH >= 500 ? 5 : 4;
const IMAGES_ROWS = 8;
const FETCH_IMAGES_PER_REQUEST = RENDER_IMAGES_PER_ROW * IMAGES_ROWS;
const WH = Math.floor((SCREEN_WIDTH - PADDING) / RENDER_IMAGES_PER_ROW);
const IMAGE_WIDTH = WH;
const IMAGE_HEIGHT = WH;

    const initialImagesInOrder: string[] = [];
    const [imagesInOrder, setImagesInOrder] = useState(initialImagesInOrder);
    const [media, setMedia] = useState([]);
    const [mediaToRender, setMediaToRender] = useState([]);
    const [after, setAfter] = useState('');
    const [checkedForMedia, setCheckedForMedia] = useState(false);
    const [initialRequest, setInitialRequest] = useState(true);
    const [hasNextPage, setHasNextPage] = useState(true);
    const [videoURI, setVideoURI] = useState('');


    const hasAndroidPermission = async () => {
        const permission =
            PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;

        const hasPermission = await PermissionsAndroid.check(permission);
        if (hasPermission) {
            return true;
        }

        const status = await PermissionsAndroid.request(permission);
        return status === 'granted';
    };

    const permissionCheck = async () => {
        if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
            return;
        }
    };

    const getLocalMedia = (_after: string | null) => {
        if (hasNextPage) {
            let params = {
                first: FETCH_IMAGES_PER_REQUEST,
                assetType: props.mediaType,
                include: ['playableDuration'],
            };

            if (_after !== null) {
                // @ts-ignore
                params.after = _after;
            }

            // @ts-ignore
            CameraRoll.getPhotos(params)
                .then(r => {
                    // @ts-ignore
                    const newImages = media.concat(r.edges);

                    setCheckedForMedia(true);
                    setInitialRequest(false);

                    // @ts-ignore
                    setMedia(newImages);

                    // @ts-ignore
                    setAfter(r.page_info.end_cursor);
                    // @ts-ignore
                    setHasNextPage(r.page_info.has_next_page);
                })
                .catch(error => {
                    modalData.modalText = `Unable to load media!`;
                    modalData.modalIsVisible = true;
                    setModalData(modalData);
                    
                });
        }
    };

    useEffect(() => {
        permissionCheck()
            .then(() => {
                setCheckedForMedia(false);
                setMedia([]);
                setInitialRequest(true);
                setHasNextPage(true);
                setImagesInOrder(initialImagesInOrder);
                setVideoURI('');
            })
            .catch(error => {
                modalData.modalText = `Something went wrong! Please try again.`;
                modalData.modalIsVisible = true;
                setModalData(modalData);

            });
    }, [props]);

    useEffect(() => {
        if (
            media.length === 0 &&
            !checkedForMedia &&
            initialRequest &&
            hasNextPage
        ) {
            getLocalMedia(null);
        }
    }, [media, checkedForMedia, initialRequest, hasNextPage]);

    useEffect(() => {
        setMediaToRender(
            media.reduce((all, one, i) => {
                const ch = Math.floor(i / RENDER_IMAGES_PER_ROW);
                // @ts-ignore
                all[ch] = [].concat(all[ch] || [], one);
                return all;
            }, []),
        );
    }, [media]);


    const handleImage = (imageURI: string) => {
        if (imagesInOrder.includes(imageURI)) {
            setImagesInOrder(imagesInOrder.filter(i => i !== imageURI));
        } else {
            if (
                props.maxImagesPerPost !== undefined &&
                props.maxImagesPerPost !== null &&
                imagesInOrder.length < props.maxImagesPerPost
            ) {
                setImagesInOrder([...imagesInOrder, imageURI]);
            }
        }
    };

    // @ts-ignore
    const renderItem = ({ item }) => {
        if (props.mediaType === 'Photos') {
            return renderImages(item);
        } else {
            return renderVideos(item);
        }
    };

    const renderImageBackground = ({
        uri,
        data,
        seconds,
    }: {
        uri: string;
        data: any | null;
        seconds: number | null;
    }) => (
        <ImageBackground
            style={styles.imageBackgroundStyle}
            source={{ uri }}
            resizeMode="cover"
            key={uri}>
            {data !== undefined && data !== null && (
                <View style={styles.imageBackgroundChild}>
                    <View style={styles.imageSelectedView}>{data}</View>
                </View>
            )}
            {data == null && seconds !== null && (
                <View
                    style={
                        props.maxVideoLengthInSeconds !== undefined &&
                        seconds > props.maxVideoLengthInSeconds
                            ? {
                                    ...styles.secondsViewForVideo,
                                    ...styles.videoDisabled,
                              }
                            : {
                                    ...styles.secondsViewForVideo,
                              }
                    }>
                    <Text
                        style={
                            props.maxVideoLengthInSeconds !== undefined &&
                            seconds > props.maxVideoLengthInSeconds
                                ? styles.disabledSecondsTextForVideo
                                : styles.secondsTextForVideo
                        }>
                        {formatSecondsForVideo(seconds.toString())}
                    </Text>
                </View>
            )}
        </ImageBackground>
    );

    const renderImages = (item: any) => (
        <View style={styles.mediaView}>
            {
                // @ts-ignore
                item.map(i => {
                    const uri = i.node.image.uri;
                    const data = imagesInOrder.includes(uri) ? (
                        <Text style={styles.imageSelectedText}>
                            {imagesInOrder.indexOf(uri) + 1}
                        </Text>
                    ) : null;
                    const seconds = null;

                    return (
                        <View
                            style={styles.mediaSubView}
                            key={Helper.generateRandomKey()}>
                            <TouchableOpacity
                                onPress={() => {
                                    handleImage(uri);
                                }}>
                                {renderImageBackground({
                                    uri,
                                    data,
                                    seconds,
                                })}
                            </TouchableOpacity>
                        </View>
                    );
                })
            }
        </View>
    );

    const handleMedia = () => {
        if (checkedForMedia && media.length === 0) {
            return (
                <View style={[styles.containerForNoImage]}>
                    <Text style={[styles.text]}>
                        No {props.mediaType === 'Photos' ? `photo` : `video`}{' '}
                        found!
                    </Text>
                </View>
            );
        }

        return (
            <FlatList
                data={mediaToRender}
                keyExtractor={(item, index) => index.toString()}
                // @ts-ignore
                renderItem={renderItem}
                initialNumToRender={5}
                maxToRenderPerBatch={10}
                windowSize={10}
                onEndReachedThreshold={0.5}
                onEndReached={() => {
                    getLocalMedia(after);
                }}
            />
        );
    };

So my logic is straight forward. Add or remove image from state array and update rendered image if its path exist in array. If path exist, get index and update the view. But I am not sure why it is not performing fast enough in my test device! The order array is not crazy big that should cause problem. As you can see in gif that blur view gets rendered right away but text in middle doesn't render with it.

Also, I have shared only chunk of code that handles images stuff! If I have missed something, please let me know and I will update code! Also when I scroll images or this view first gets rendered, it loads images slowly! Any suggestion to load images faster? They are locally stored on device and not getting fetched from remote server!

Thanks in advance for you help!


Solution

  • So I was able to fix it with help of azundo's recommendation. It was silly mistake of mine. I was using random key everytime it rendered image view! But after using Image URI, it fixed the issue of slow render and now it is working much better.

    As @azundo said,

    Can you try using a the uri as the key for the images instead of a random one? Could be causing some unnecessary thrashing on the rendering side if it thinks those are all new elements every render. If uri isn't unique the generate the random key once and store it alongside each image instead of creating a new one each render.

    Thanks @azundo! Also thank you @docmurloc for your time and help.