React – PureRenderMixinの検証

このエントリーをはてなブックマークに追加

PureRenderMixin

はじめに

 ReactにはPureRenderMixinという今のところ(0.12.2) addonとして提供されているMixinがあります。どういうものかと言うと

If your React component’s render function is “pure” (in other words, it renders the same result given the same props and state), you can use this mixin for a performance boost in some cases.

対象となるコンポーネントが「純粋」(renderが同じpropsstateを与えられて必ず出力結果が同じになる)であれば、パフォーマンス向上が見込めるmixin。

と本家で述べられています。

シンプルな仕様検証

PureRenderMixin無し

 さっそくこれを実際に試してみます。以下、PureRenderMixin無しでvalueというstateをボタンのクリックでインクリメントまたは同一値を設定(setState)します。

 ’Count Up Event’ボタンをクリックした場合はvalueの値が変わりますし、それに伴ってrenderも呼ばれています。そして、’Set Same Value Event’ボタンをクリックした場合もvalueが変わらないにも関わらずrenderが呼ばれているのがわかります。これは無駄な処理が走っていることになります。

PureRenderMixin有り

 では、PureRenderMixinを追加するとどうなるのか?以下はMixinを追加したバージョンです。

 違いがわかるのは’Set Same Value’ボタンをクリックしたときです、renderが呼ばれていないことがわかります。

どうなってるの?

 PureRenderMixinが何をしているのか気になるところです。オープンソースなので簡単に観ることができます。

var ReactComponentWithPureRenderMixin = {
  shouldComponentUpdate: function(nextProps, nextState) {
    return !shallowEqual(this.props, nextProps) ||
           !shallowEqual(this.state, nextState);
  }
};

 shouldComponenUpdateを使って、propsstateに対して等値であるかどうか比較しています。等値の比較方法にはshallowEqualを使っている様ですね。このshallowEqualの仕様がとても重要なポイントです。

function shallowEqual(objA, objB) {
  if (objA === objB) {
    return true;
  }
  var key;
  // Test for A's keys different from B.
  for (key in objA) {
    if (objA.hasOwnProperty(key) &&
        (!objB.hasOwnProperty(key) || objA[key] !== objB[key])) {
      return false;
    }
  }
  // Test for B's keys missing from A.
  for (key in objB) {
    if (objB.hasOwnProperty(key) && !objA.hasOwnProperty(key)) {
      return false;
    }
  }
  return true;
}
  • AとBが同一値、同一参照先であればtrue
  • AのプロパティがBになかったらfalse
  • AのプロパティがBにあって、値または参照先が違ってたらfalse
  • BのプロパティがAになければfalse
  • 上のどれにもあてはまらなければtrue

 ここで、先ほどの例に戻った場合に’Set Same Value Event’ボタンはhandleNoChangeClickを実行し、その中ではsetStateを呼んでいるけど、元のthis.state.valuevalueに設定していることに注目してください。

handleNoChangeClick : function() {
  // intentionally set the same value
  this.setState({value : this.state.value});
}

 これは同一値として扱われるのでshouldComponentUpdateにてfalse扱いとなり、renderが走らないのです。

要注意事項

 いちいち自分で書かなくても済むようになるのでとても便利そうに見えるPureRenderMixinですが、仕様ははっきりと理解しておかないとまずそうです。本家にも赤字でしっかりと書いてあります。

This only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only mix into components which have simple props and state, or use forceUpdate() when you know deep data structures have changed.

あくまでもオブジェクトを比較する際には浅い比較しか行いません。複雑な構造(深い構造)をもったオブジェクト比較を行った場合は意図しない結果を生む可能性があります。単純なpropsstateを持つコンポーネントにだけ使うか、深い階層で更新があったことがわかった場合はforceUpdate()を使ってください。

 どういうことかと言うと、以下のような極端なコードを書いてみます。(下記のようなコードを書くことなんてありえませんが、ぱっといい例が思い浮かばなかったのでやむを得ず。)

 これはPureRenderMixinを含めていないのでsetStateを呼ぶ度に自動的にrenderが走り、ボタンに対応した値が更新されていくのが確認できます。それではこれにPureRenderMixinを含めてみましょう。

 ’increment’ボタンをクリックしても値が変わりませんね。これが’shallowEqual’の仕様です。比較するのは「値」か「参照先」が一緒かどうかです。values配列の中の値を変えているのですが、values自体の「参照先」は変えていません。よって、PureRenderMixinはrenderする必要が無いと思い込んでしまい、再描画が走らないのです。何回か’increment’をクリックして’Force Render’ボタンをクリックしたら一気に値が変わるのが確認できるかと思います。

シンプルなパフォーマンス検証

PureRenderMixin無し

コード

 それではPureRenderMixin無しでシンプルなパフォーマンス検証をしてみましょう。先ほどと同じような例ですが、今回は1行1子コンポーネントとして作り直します。また、便利のためImmutableJSも使いました。5,000行使って各行の値を更新するボタンを用意しています。また、各々の子コンポーネントに何回renderが走ったかわかるようにカウンタをつけています。

結果

‘increment’ボタンを押す度に全ての親・兄弟コンポーネントも再描画が走っているのがわかるしもっさりしているのが体感できると思います。パフォーマンスも再描画に1.1秒ほどかかっているのがわかります。

スクリーンショット 2015-03-08 14.40.42

スクリーンショット 2015-03-08 14.41.26

PureRenderMixin有り

コード

 上の例でPureRenderMixinを有効にします。

 今回は’increment’ボタンを押した場合は親とその特定の子コンポーネントのみが再描画されているのがわかります。また、パフォーマンスも0.18秒と、飛躍的に向上しています。

スクリーンショット 2015-03-08 14.41.58

スクリーンショット 2015-03-08 14.41.31

 このコンポーネントの親子関係とstateの更新方法がFLUXの思想に則っているのか疑問が残っていますが、PureRenderMixinの検証という意味では方向性はさほど間違っていないかと思います。これを念頭に置いて今後の開発に活かしていきたいですね。

Written on March 8, 2015
このエントリーをはてなブックマークに追加