web-dev-qa-db-ja.com

Reactパフォーマンス: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でshouldComponentUpdateを呼び出してから、単一のTodoをレンダリングするのにますます時間がかかります。

ご覧のとおり、TodoコンポーネントにはTodoデータ以外のコンポーネントがあります。これは、すべてのtodoによるレンダリングに必要であり、共有されるデータです(たとえば、todoの「displayMode」があると想像できます)。多くのプロパティがあると、shouldComponentUpdateのパフォーマンスが少し遅くなります。

また、ReactCSSTransitionGroupは、todosのReactCSSTransitionGroupが呼び出される前であってもReactCSSTransitionGroupChild自体とshouldComponentUpdateをレンダリングする必要があるため、少し遅くなるようです。 React.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ノードを作成する必要があるかもしれません。これは、ユースケース(- およびReactの将来のバージョンで解決される可能性があります

また、Javascriptを使用しているので、Immutable-JSがこれをサポートしていて、「内部ノード」にアクセスできるようにしているのでしょうか。参照: https://github.com/facebook/immutable-js/issues/541

編集:私の実験との便利なリンク: React-Reduxアプリは本当にバックボーンと同様にスケーリングできますか?再選択を使用しても。モバイル

24

これが、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を呼び出す必要はありません。

実装の詳細はこれで共有されます ImmutableJs github issue

5

私たちの製品では、レンダリングされるコードの量に関連する問題もあり、オブザーバブルを使用し始めました(これを参照してください ブログ )。 todoを変更すると、リストを保持する親コンポーネントを再レンダリングする必要がなくなるため、これで問題が部分的に解決する可能性があります(ただし、追加してもまだレンダリングされます)。

また、プロップがshouldComponentUpdateで変更されたときにtodoItemコンポーネントがfalseを返すだけなので、リストをより速く再レンダリングするのにも役立ちます。

概要をレンダリングする際のパフォーマンスをさらに向上させるために、ツリー/ページングのアイデアは本当にすばらしいと思います。オブザーバブルアレイを使用すると、各ページが特定の範囲内のアレイスプライス(ES7ポリフィルまたはモニター可能を使用)をリスニングし始める可能性があります。これらの範囲は時間の経過とともに変化する可能性があるため、これにより管理が導入されますが、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にシフトするだけではできないようです。それ以外の場合は、データを小さなチャンクに分割するために「ページ」をまったく必要とせず、この jsFiddle のJQueryで行ったように、1つの新しいTodoアイテムをdomにスプライスするだけで済みます。 ToDoアイテムごとにrefを使用すれば、reactを使用してそれを行うこともできますが、調整システムが壊れる可能性があるので、システムで問題なく動作していると思いますか?

9
mweststrate

最近、500以上のレコードを持つテーブルをレンダリングしようとするパフォーマンスのボトルネックがあり、レデューサーは不変で、複雑なセレクターをメモするためにreselectを使用していました。髪を引っ張った後、すべてのセレクターをメモして問題が解決されたことがわかりました。

0
Kornelius