Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

react-virtualized WindowScroller performance issues

I am using the react-virtualized library to create efficient news feed. The library is awesome. I combined WindowScroller, AutoSizer and VirtualScroll components to have inifinite scrolling behavior. The problem is that when I set the VirtualScroll height manually and don't use WindowScroller the performance is great in all browsers. However when I add the WindowScroller component, performance reduces significantly, especially in Firefox (v47.0). How can I optimize this so that it is feasible to use window scrolling?

This is the News component, where the react-virtualized is used, I have 2 types of list items - header item and simple item, header item contains a date of a group of news, thus it is a little bit longer.

import React, { PropTypes, Component } from 'react';
import Divider from 'material-ui/Divider';
import Subheader from 'material-ui/Subheader';
import { Grid, Row, Col } from 'react-flexbox-grid';
import NewsItem from '../NewsItem';
import styles from './styles.css';
import CircularProgress from 'material-ui/CircularProgress';
import Paper from 'material-ui/Paper';
import classNames from 'classnames';
import { InfiniteLoader, WindowScroller, AutoSizer, VirtualScroll } from 'react-virtualized';
import shallowCompare from 'react-addons-shallow-compare';

class News extends Component {

  componentDidMount() {
    this.props.onFetchPage(0);
  }

  shouldComponentUpdate(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  }

  getRowHeight({ index }) {
    const elementHeight = 200;
    const headerHeight = 78;
    if (!this.isRowLoaded(index)) {
      return elementHeight;
    }
    return this.props.articles[index].isHeader ?
      headerHeight + elementHeight : elementHeight;
  }

  displayElement(article, isScrolling) {
    return (
      <Paper
        key={article.id}
        className={classNames(styles.newsItemContainer, {
          [styles.scrolling]: isScrolling
        })}
      >
        <NewsItem {...article} />
        <Divider />
      </Paper>
    );
  }

  isRowLoaded(index) {
    return !this.props.hasNextPage || index < this.props.articles.length;
  }

  renderRow(index, isScrolling) {
    if (!this.isRowLoaded(index)) {
      return (
        <div className={styles.spinnerContainer}>
          {this.props.isFetching ? <CircularProgress /> : null}
        </div>
      );
    }
    const { isHeader, date, article } = this.props.articles[index];
    if (isHeader) {
      return (
        <div>
          <Subheader
            key={date}
            className={styles.groupHeader}
          >
            {date}
          </Subheader>
          {this.displayElement(article, isScrolling)}
        </div>
      );
    }
    return this.displayElement(article, isScrolling);
  }

  noRowsRenderer() {
    return (<p>No articles found</p>);
  }

  render() {
    const {
      articles,
      onFetchPage,
      pageNumber,
      isFetching,
      hasNextPage
    } = this.props;

    const loadMoreRows = isFetching ?
      () => {} :
      () => onFetchPage(pageNumber + 1);

    const rowCount = hasNextPage ? articles.length + 1 : articles.length;

    return (
      <Grid>
        <Row>
          <Col xs={12} sm={8} smOffset={2}>
            <InfiniteLoader
              isRowLoaded={({ index }) => this.isRowLoaded(index)}
              loadMoreRows={loadMoreRows}
              rowCount={rowCount}
            >
              {({ onRowsRendered, registerChild, isScrolling }) => (
                <WindowScroller>
                  {({ height, scrollTop }) => (
                    <AutoSizer disableHeight>
                      {({ width }) => (
                        <VirtualScroll
                          autoHeight
                          ref={registerChild}
                          height={height}
                          rowCount={rowCount}
                          rowHeight={(...args) => this.getRowHeight(...args)}
                          rowRenderer={({ index }) => this.renderRow(index, isScrolling)}
                          width={width}
                          noRowsRenderer={this.noRowsRenderer}
                          onRowsRendered={onRowsRendered}
                          overscanRowCount={10}
                          scrollTop={scrollTop}
                        />
                      )}
                    </AutoSizer>
                  )}
                </WindowScroller>
              )}
            </InfiniteLoader>
          </Col>
        </Row>
      </Grid>
    );
  }
}

News.propTypes = {
  articles: PropTypes.array.isRequired,
  onFetchPage: PropTypes.func.isRequired,
  isFetching: PropTypes.bool.isRequired,
  pageNumber: PropTypes.number.isRequired,
  hasNextPage: PropTypes.bool.isRequired
};

export default News;

And the list item is the following component:

import React, { PropTypes } from 'react';
import styles from './styles.css';
import { Row, Col } from 'react-flexbox-grid';
import shallowCompare from 'react-addons-shallow-compare';
import pick from 'lodash/pick';
import NewsItemContent from '../NewsItemContent';

class NewsItem extends React.Component {

  shouldComponentUpdate(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  }

  render() {
    const contentProps = pick(this.props, [
      'title', 'description', 'seedUrl', 'seedCode', 'date'
    ]);
    return (
      <div
        onClick={() => window.open(this.props.url, '_blank')}
        className={styles.newsItem}
      >
        {this.props.imageUrl ?
          <Row>
            <Col xs={3}>
              <div
                role="presentation"
                style={{ backgroundImage: `url(${this.props.imageUrl})` }}
                className={styles.previewImage}
              />
            </Col>
            <Col xs={9}>
              <NewsItemContent {...contentProps} />
            </Col>
          </Row> :
          <Row>
            <Col xs={12}>
              <NewsItemContent {...contentProps} />
            </Col>
          </Row>
        }
      </div>
    );
  }
}

NewsItem.propTypes = {
  imageUrl: PropTypes.string,
  description: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
  url: PropTypes.string.isRequired,
  date: PropTypes.object.isRequired,
  seedUrl: PropTypes.string.isRequired,
  seedCode: PropTypes.string.isRequired
};

export default NewsItem;

NewsItemContent here is a simple pure component without any logic, so I won't put it here.

Thank you!

Update: I've recorded performance timelines in firefox both in case of window scrolling and block scrolling:

  • Block scrolling
  • Window scrolling
like image 409
Max Semikin Avatar asked Nov 09 '22 11:11

Max Semikin


1 Answers

I think this has to do with React setState() calls.

WindowScroller listens to scroll events for the window object. These happen outside of React's knowledge so setState() calls get handled by the ReactDefaultBatchingStrategy which is synchronous. Grid is a React component so its onScroll events happen within React's awareness. setState() calls that occur as a result can be batched more intelligently by ReactUpdateQueue.

Looking at the timelines you've shared, setState calls for Grid scroll events get enqueued by ReactUpdateQueue. But looking at the WindowScroller timeline- where the frame rate is the worst, its setState() calls are being immediately passed to the ReactDefaultBatchingStrategy.

like image 158
bvaughn Avatar answered Nov 15 '22 07:11

bvaughn