反应性能:使用PureRenderMixin

时间:2015-06-22 09:43:03

标签: javascript reactjs immutable.js

我拿了一个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更新时,渲染父节点需要花费越来越多的时间,在所有todos上调用shouldComponentUpdate,然后渲染单个待办事项。

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

此外,使用ReactCSSTransitionGroup似乎也会慢下来,因为ReactCSSTransitionGroup甚至必须在调用ReactCSSTransitionGroupChild todos之前呈现自己和shouldComponentUpdateReact.addons.Perf表示ReactCSSTransitionGroup > ReactCSSTransitionGroupChild呈现是列表中每个项目浪费的时间。

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

有什么想法吗?

修改

到目前为止,我的大列表是分页的,所以我现在将这个大列表分成一个页面列表,而不是列出大项目。这样可以获得更好的性能,因为每个页面现在都可以实现shouldComponentUpdate。现在,当一个项目在页面中发生变化时,React只需要调用在页面上迭代的主渲染函数,并且只从单个页面调用渲染函数,这样可以减少迭代次数。

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

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

来短路计算

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

http://hypirion.com/imgs/pvec/9-annotated.png

然而,我关注React,因为据我所知,在渲染树的内部节点时可能需要创建中间dom节点。根据用例(and may be solved in future versions of React

,这可能是一个问题

同样,当我们使用Javascript时,我想知道Immutable-JS是否支持这一点,并且制作&#34;内部节点&#34;无障碍。请参阅:https://github.com/facebook/immutable-js/issues/541

修改:与我的实验有用的链接:Can a React-Redux app really scale as well as, say Backbone? Even with reselect. On mobile

3 个答案:

答案 0 :(得分:9)

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

它还可以帮助您更快地重新呈现列表,因为当在propsComponentUpdate上更改props时,todoItem组件可能只返回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>,
     );
  }

});

大型列表和反应的核心问题似乎是您无法将新的DOM节点转移到dom中。但是,你不会需要这些页面&#39;完全按照较小的块来划分数据,你可以将一个新的Todo项目拼接到dom中,就像在jsFiddle中使用JQuery一样。如果你对每个待办事项使用ref,你仍然可以做出反应,但是这会解决系统问题,因为它可能会破坏对帐系统?

答案 1 :(得分:5)

这是我用ImmutableJS内部结构完成的POC实现。这不是一个公共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 < / p>

ImmutableJs github issue

分享了更多实施细节

答案 2 :(得分:0)

最近我有一个性能瓶颈试图渲染一个包含500多条记录的表,减速器是不可变的,我使用重新选择来记忆复杂的选择器,在拉了一些头发之后,我发现问题解决了所有选择器的记忆。