React 性能:使用 PureRenderMixin 渲染大列表

React performance: rendering big list with PureRenderMixin

我举了一个 TodoList 的例子来反映我的问题,但显然我的实际代码更复杂。

我有一些这样的伪代码。

var Todo = React.createClass({
  mixins: [PureRenderMixin], 
  ............ 
}

var TodosContainer = React.createClass({
  mixins: [PureRenderMixin],    

  renderTodo: function(todo) {
     return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
  },

  render: function() {
     var todos = this.props.todos.map(this.renderTodo)
     return (
          <ReactCSSTransitionGroup transitionName="transition-todo">
                 {todos}
          </ReactCSSTransitionGroup>,
     );
  }

});

我所有的数据都是不可变的,适当使用 PureRenderMixin 一切正常。当一个 Todo 数据被修改时,只有父 Todo 和修改后的 Todo 会被重新渲染。

问题是在某些时候我的列表会随着用户滚动而变大。并且当更新单个 Todo 时,渲染父级需要越来越多的时间,对所有 todo 调用 shouldComponentUpdate,然后渲染单个 todo。

如您所见,Todo 组件除了 Todo 数据之外还有其他组件。这是所有待办事项渲染所需的数据,并且是共享的(例如,我们可以想象待办事项有一个 "displayMode")。具有许多属性会使 shouldComponentUpdate 执行速度稍慢。

此外,使用 ReactCSSTransitionGroup 似乎也有点慢,因为 ReactCSSTransitionGroup 必须渲染自己,而 ReactCSSTransitionGroupChild 甚至在调用 todos 的 shouldComponentUpdate 之前。 React.addons.Perf 显示 ReactCSSTransitionGroup > ReactCSSTransitionGroupChild 渲染是列表中每个项目的浪费时间。

因此,据我所知,我使用 PureRenderMixin,但对于更大的列表,这可能还不够。我仍然没有那么糟糕的表现,但想知道是否有简单的方法来优化我的渲染。

有什么想法吗?


编辑:

到目前为止,我的大列表是分页的,所以我现在不再使用大列表,而是将这个大列表拆分为页面列表。这允许有更好的性能,因为每个页面现在可以实现 shouldComponentUpdate。现在当页面中的项目发生变化时,React 只需要调用在页面上迭代的主要渲染函数,并且只从单个页面调用渲染函数,这大大减少了迭代工作。

但是,我的渲染性能仍然与我拥有的页码 (O(n)) 成线性关系。所以如果我有数千页它仍然是同样的问题 :) 在我的用例中它不太可能发生但我仍然对更好的解决方案感兴趣。

我很确定可以通过将大列表拆分成树(如持久数据结构)来实现 O(log(n)) 渲染性能,其中 n 是项目(或页面)的数量,并且每个节点都有权使用 shouldComponentUpdate

使计算短路

是的,我正在考虑类似于 Scala 或 Clojure 中的 Vector 等持久数据结构的东西:

但是我担心 React,因为据我所知,它可能必须在渲染树的内部节点时创建中间 dom 节点。根据用例,这可能是个问题 (and may be solved in future versions of React)

同时我们正在使用 Javascript 我想知道 Immutable-JS 是否支持这个,并使 "internal nodes" 可访问。参见:https://github.com/facebook/immutable-js/issues/541

编辑:对我的实验有用link:Can a React-Redux app really scale as well as, say Backbone? Even with reselect. On mobile

在我们的产品中,我们也遇到了与渲染代码量相关的问题,因此我们开始使用可观察对象(参见 blog)。这可能会部分解决您的问题,因为更改待办事项将不再需要重新呈现包含列表的父组件(但添加仍然需要)。

它还可以帮助您更快地重新呈现列表,因为当 shouldComponentUpdate 上的道具发生变化时,您的 todoItem 组件可能 return false。

为了在渲染概览时进一步提高性能,我认为您的树/分页想法确实不错。使用可观察数组,每个页面都可以开始监听特定范围内的数组拼接(使用 ES7 polyfill 或 mobservable)。这将引入一些管理,因为这些范围可能会随着时间的推移而变化,但应该让你达到 O(log(n))

所以你得到类似的东西:

var TodosContainer = React.createClass({
  componentDidMount() {
     this.props.todos.observe(function(change) {
         if (change.type === 'splice' && change.index >= this.props.startRange && change.index < this.props.endRange)
             this.forceUpdate();
     });
  },    

  renderTodo: function(todo) {
     return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
  },

  render: function() {
     var todos = this.props.todos.slice(this.props.startRange, this.props.endRange).map(this.renderTodo)
     return (
          <ReactCSSTransitionGroup transitionName="transition-todo">
                 {todos}
          </ReactCSSTransitionGroup>,
     );
  }

});

大型列表和 React 的核心问题似乎是您不能将新的 DOM 节点转移到 dom 中。否则,您根本不需要 'pages' 来将数据分成更小的块,您只需将一个新的 Todo 项目拼接到 dom 中,就像 JQuery 中所做的那样 jsFiddle。如果你对每个待办事项使用 ref,你仍然可以通过 React 来做到这一点,但我认为这会绕过系统,因为它可能会破坏协调系统?

这是我用 ImmutableJS 内部结构完成的 POC 实现。这不是 public API,所以它还没有准备好投入生产,目前不处理极端情况,但它可以工作。

var ImmutableListRenderer = React.createClass({
  render: function() {
    // Should not require to use wrapper <span> here but impossible for now
    return (<span>
        {this.props.list._root ? <GnRenderer gn={this.props.list._root}/> : undefined}
        {this.props.list._tail ? <GnRenderer gn={this.props.list._tail}/> : undefined}
</span>);
  }   
})

// "Gn" is the equivalent of the "internal node" of the persistent data structure schema of the question
var GnRenderer = React.createClass({
    shouldComponentUpdate: function(nextProps) {
      console.debug("should update?",(nextProps.gn !== this.props.gn));
      return (nextProps.gn !== this.props.gn);
    },
    propTypes: {
        gn: React.PropTypes.object.isRequired,
    },
    render: function() {
        // Should not require to use wrapper <span> here but impossible for now
        return (
            <span>
                {this.props.gn.array.map(function(gnItem,index) { 
                    // TODO should check for Gn instead, because list items can be objects too...
                    var isGn = typeof gnItem === "object"
                    if ( isGn ) {
                        return <GnRenderer gn={gnItem}/>
                    } else {
                        // TODO should be able to customize the item rendering from outside
                        return <span>{" -> " + gnItem}</span>
                    }
                }.bind(this))}
            </span>
        );
    }
})

客户端代码看起来像

React.render(
    <ImmutableListRenderer list={ImmutableList}/>, 
    document.getElementById('container')
);

这里是一个 JsFiddle,它记录了更新列表的单个元素(大小 N)后 shouldComponentUpdate 调用的次数:这不需要调用 N 次 shouldComponentUpdate

在此 ImmutableJs github issue

中分享了更多实施细节

最近我在尝试渲染包含 500 多条记录的 table 时遇到了性能瓶颈,reducer 是 immutable 并且我使用 reselect 来记忆复杂的选择器,在拉了一些头发之后,我发现问题已解决,记住了所有选择器。