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:
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With