react-virtualized WindowScroller 性能问题

Posted

技术标签:

【中文标题】react-virtualized WindowScroller 性能问题【英文标题】:react-virtualized WindowScroller performance issues 【发布时间】:2016-12-03 02:53:20 【问题描述】:

我正在使用 react-virtualized 库来创建高效的新闻提要。图书馆真棒。我结合了 WindowScroller、AutoSizer 和 VirtualScroll 组件来实现无限滚动行为。问题是当我手动设置 VirtualScroll 高度并且不使用 WindowScroller 时,所有浏览器的性能都很好。但是,当我添加 WindowScroller 组件时,性能会显着降低,尤其是在 Firefox (v47.0) 中。我该如何优化这一点,以便使用窗口滚动是可行的?

这是 News 组件,这里使用了 react-virtualized, 我有 2 种类型的列表项 - 标题项和简单项,标题项包含一组新闻的日期,因此它有点长。

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;

并且列表项是以下组件:

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是一个简单的纯组件,没有任何逻辑,这里就不放了。

谢谢!

更新: 我已经在 Firefox 中记录了窗口滚动和块滚动的性能时间表:

Block scrolling Window scrolling

【问题讨论】:

看起来你的 isScrolling 参数在错误的函数中(所以它永远不会是真的)。那应该来自WindowScroller(不是InfiniteLoader)。不确定这是否会对您的滚动性能产生很大影响,因为我不确定该参数的用途。 您有没有机会指出我可以并排查看 2 并查看时间轴的地方? @brianvaughn 感谢您注意到 isScrolling 来自错误的位置,但它并没有解决滚动问题。我将尝试创建一个 Plunker 来演示这个问题。 @brianvaughn 添加了性能记录(都是在 react 开发模式下记录的,所以这些都比优化的构建慢一些),希望这会有所帮助。很难把我的例子举给 Plunker,但如果它有所作为,我绝对会的。 【参考方案1】:

我认为这与 React setState() 调用有关。

WindowScroller 监听 window 对象的滚动事件。这些发生在 React 的知识之外,所以 setState() 调用由同步的 ReactDefaultBatchingStrategy 处理。 Grid 是一个 React 组件,因此它的 onScroll 事件发生在 React 的感知范围内。 setState() 作为结果发生的调用可以通过ReactUpdateQueue 更智能地进行批处理。

查看您共享的时间线,setState 调用 Grid 滚动事件被 ReactUpdateQueue 排队。但是看看WindowScroller 时间线——帧速率最差的地方,它的setState() 调用会立即传递给ReactDefaultBatchingStrategy

【讨论】:

老实说,我对 React 的这一面不是很熟悉。我会将我的回复添加为评论而不是“答案”,但格式很不稳定,长度限制太短。 感谢您查看它,据我了解,它无法真正优化。我尝试使用 react-infinite 实现相同的列表,因为它还可以选择对窗口滚动事件做出反应,并且所有浏览器中的帧速率都要好得多(仅考虑窗口滚动),这对我来说很奇怪。 有趣。因此,使用等效的 WindowScroller 从 react-infinite 获得更好的性能?如果是这样,我将不得不检查他们的实施。 是的,他们有一个名为 useWindowAsScrollContainer 的选项,它做同样的事情。

以上是关于react-virtualized WindowScroller 性能问题的主要内容,如果未能解决你的问题,请参考以下文章

react 分页器 基于react-virtualized组件的分页器

React-virtualized 中对 registerChild 的 CellMeasurer 支持

React-virtualized - 是不是可以在窗口调整大小时更新 rowHeights?

React-virtualized 动态高度列表呈现最初堆叠的所有内容

react-virtualized WindowScroller 性能问题

有没有办法通过 react-virtualized 将 ref 设置为 List ?