Search code examples
reactjsperformancescrollinfinite-scrollreact-virtualized

React-virtualized list running below 60fps. How to optimize?


I am trying to create a post feed like the one instagram has (on the main page).

I'm using Infinite-loader for fetching, Window-scroller for using the window as the scroll, auto-sizer for sizing the list how i want and CellMeasurer for measuring the 'post component' once after the image has been loaded.

Here is code for the list component:

class PostsPartial extends React.PureComponent<IProps>{
state: IPostsPartialState = { posts: [], hasMorePosts: true }

private cache: CellMeasurerCache;

private get rowCount(): number {
    return this.state.hasMorePosts ? this.state.posts.length + 1 : this.state.posts.length;
}

constructor(props: IProps) {
    super(props);

    this.cache = new CellMeasurerCache({
        fixedWidth: true,
        defaultHeight: 1000
    });

    this.renderRow = this.renderRow.bind(this);
}

private fetchPosts = ({ startIndex, stopIndex }: { startIndex: number, stopIndex: number }) => {
    return getNewPostsChunk(startIndex, stopIndex - startIndex, this.props.token).then((res: IPostsChunkResponse) => {
        if (res.success) {
            if (res.posts.length === 0) {
                // no more posts
                this.setState({ hasMorePosts: false })
            }
            else {
                let newPosts = [...this.state.posts, ...res.posts];
                this.setState({ posts: newPosts })
            }
        }
        else {
            // internal error
        }
    })
};

private renderRow({ index, key, parent, style }: any) {
    return (
        <CellMeasurer
            cache={this.cache}
            columnIndex={0}
            key={key}
            parent={parent}
            rowIndex={index}
        >
            {({ measure, registerChild }: any) => (
                <div className={styles.paddingContainer} ref={registerChild} style={style}>
                    <Post
                        isLoaded={this.isRowLoaded({index})}
                        measure={measure}
                        post={this.state.posts[index]}
                    />
                </div>
            )}
        </CellMeasurer>
    );
}

private isRowLoaded = ({ index }: { index: number }) => {
    return !!this.state.posts[index];
};

public render() {
    return (
        <div className={styles.mainContainer}>
            <InfiniteLoader
                isRowLoaded={this.isRowLoaded}
                loadMoreRows={this.fetchPosts}
                rowCount={this.rowCount}
            >
                {({ onRowsRendered, registerChild }: InfiniteLoaderChildProps) => (
                    <WindowScroller>
                        {({ height, isScrolling, onChildScroll, scrollTop }) => (
                            <AutoSizer disableHeight>
                                {
                                    ({ width }: any) => (
                                        <List
                                            ref={registerChild}
                                            onRowsRendered={onRowsRendered}
                                            autoHeight
                                            width={width}
                                            height={height}
                                            isScrolling={isScrolling}
                                            onScroll={onChildScroll}
                                            scrollTop={scrollTop}
                                            deferredMeasurementCache={this.cache}
                                            rowHeight={this.cache.rowHeight}
                                            rowRenderer={this.renderRow}
                                            rowCount={this.rowCount}
                                            overscanRowCount={10}
                                        />
                                    )
                                }
                            </AutoSizer>
                        )}
                    </WindowScroller>
                )}
            </InfiniteLoader>
        </div>
    );
}

and here is code for the post component:

const Post:React.FC<IProps> = (props:IProps) => {
    if(props.post && props.isLoaded)
    return (
        <div className={styles.container}>
            <Segment className={styles.profileSegmentInternal} attached='top'>
                <Image className={styles.verySmallImg} circular size='tiny' src={`${settings.BASE_URL}/feed/photo/user/${props.post.creator}`}></Image>
                <Link to={`/profile/${props.post.creator}`}>
                    <Header size='small' className={styles.headerName} as='span'>{props.post.creator}</Header>
                </Link>
            </Segment>
            <div className={styles.imageContainer}>
                <Image onLoad={props.measure} src={`${settings.BASE_URL}/feed/photo/post/${props.post._id}`} className={styles.image}></Image>
            </div>
            
            <Segment className={styles.bottomSegment} attached='bottom'>
                <>
                    <Menu className={styles.postMenu}>
                        <Item className='left'>
                            <Icon className={styles.iconBtn} size='big' name='heart outline'></Icon>
                            <Icon className={styles.iconBtn} size='big' name='comment outline'></Icon>
                            <Icon className={styles.iconBtn} size='big' name='paper plane outline'></Icon>
                        </Item>
                        <Item className='right'>
                            <Icon className={styles.iconBtn} size='big' name='bookmark outline'></Icon>
                        </Item>
                    </Menu>
                </>
                <Header className={styles.likes} size='tiny'>{props.post.likesCount} likes</Header>
                <Header className={styles.description} size='tiny'>
                    <Header size='tiny' className={styles.commentUsername} as='span'>{props.post.creator}</Header>
                    <Header className={styles.commentText} as='span' size='tiny'> {props.post.description}</Header>
                </Header>
                <Link to='#'>
                    <Header className={styles.viewAllComments} size='tiny' disabled>View all comments</Header>
                </Link>
                {
                    //backend will return the first 3-4 messeges only
                    // props.post.messeges.map((messege,index) => (

                    // ))
                }
                <Form className={styles.commentForm}>
                    <Form.Field className={styles.commentField}>
                        <Form.Input
                            className={styles.commentInput}
                            placeholder='Adding comment ...'
                        >

                        </Form.Input>
                        <Button className={styles.commentSubmit} size='medium' primary>Comment</Button>
                    </Form.Field>
                </Form>
            </Segment>
        </div>
    )
    else
    return (
        <p>loading</p>
    )

Even if I remove everything from the post component and leave only the image, it still won't run with more then 45-50fps sometimes going under 40fps too.

Can I optimize my approach in any way or am I doing something wrong? Should I provide anything else that might be helpful?

Thank you in advance!


Solution

  • So I fixed my problem by resizing the image when uploading it (in the backend using sharp).

    This way the fetching is faster witch makes the (just in time) measure of a post component way faster as less data needs to be loaded on mount, and html+css doesn't have to resize the high image into a smaller container.

    Sounds silly, but I didn't think of this being the issue and instead focused on my infinite scrolling implementation :D

    Ya live and ya learn

    EDIT:

    I forgot to mention, a small change I did when setting the image src. Instead of making an express route that retrieves the src, I can just use it while receiving the post info. The file source won't be printed with console.log or whatever, but it is there and can be used like so:

    <Image className={styles.image} onLoad={props.measure} src={`data:${props.post.source.contentType};base64,${Buffer.from(props.post.source.data).toString('base64')}`} />