Aqutras Members' Blog

株式会社アキュトラスのメンバーが、技術情報などを楽しく書いています。

Reactの再レンダリングをなるべく減らす

こんにちは。maxmellon です。

今回は,Reactの再レンダリング回数をなるべく減らす方法について紹介したいと思います. 前回記事と関連があります.もしよろしければ合わせてお読みください. 本記事は,Reactのライフサイクルなどの基本的な仕様の理解のある前提で話を進めていきます。
Reactの基本的なことにつきましては,下記などを参照してください.

https://facebook.github.io/react/docs/why-react.html http://mizchi.hatenablog.com/entry/2014/09/02/201728

はじめに

Reactが再レンダリングされるケース

Reactのコンポーネントが再レンダリングされるには,次の2ケースがあります.

  • state の変化によって DOM が 追加,削除された時.
  • state の変化によって DOM の 内容が変化した時.

前者はループでDOMを組み立てるケースがほとんどです. イメージとしては,TODOアプリであれば,TODOを追加した時などが該当します. そして,このTODO一覧は,TODOの個数回ループを回すという実装をしているのがほとんどだと思います.

対して後者は,TODOの内容の更新です.すなわち setState を トリガーとして再レンダリングするようなケースです. そしてこれらは,前回記事で触れました.

ループで組み立てるDOM要素の再レンダリングの仕組みを理解する

ループを使ってDOMを組み立てるときは,ユニークな key を指定しないと警告が出ます. この key をReact側が管理することで,増減したDOMが何かを検知し, その部分だけ再レンダリングを行うという仕組みになっています.

文章だけでは,少しわかりづらいので実際にプログラムを書いて見たいと思います.

class SampleComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      array: [
        'hoge',
        'poge',
      ]
    }
    // ライフサイクル外の関数から this を参照するための bind
    this.onClick = this.onClick.bind(this);
    this.renderList = this.renderList.bind(this);
  }

  onClick() {
    let currentState = this.state.array;
    currentState.push('foo');
    this.setState({ array: currentState });
  }

  renderList() {
    return this.state.array.map(item => {
      return (
        <li key={item}>{item}</li>
      );
    });
  }

  render() {
    <div>
      <ul>
        {this.renderList()}
      </ul>
      <button onClick={this.onClick}>Add Item</button>
    </div>
  }
}

上のコンポーネントをレンダリングすると次のようになります.

  - hoge
  - poge

  [Add Item]

ここでAdd Item ボタンをおした時,追加された foo コンポーネントだけ 差分レンダリングされることを望みます.

hoge や poge が 再レンダリングされないようにするために,keyを指定します. key を 指定することによってDOMの増減があった時,最適な再レンダリングを React が 勝手にしてくれます.素晴らしいですね. また,keyを指定しておくと再レンダリング時に,指定されたkeyのComponentを再利用します
ただし,keyの指定だけでは対応しきれないものがあります. それは,増減ではなく内容の更新です. ここに関しては,詳しくは前回記事がわかりやすいですね.

データの更新に対しての差分レンダリングを最小限に抑える

データの更新に対して差分レンダリングを最小限に抑えるには,結論からいうと, shouldComponentUpdate を実装すればよいです.

では,具体的にどのような shouldComponentUpdate を実装すればよいか考えてみましょう.

shouldComponentUpdate の引数は,nextProps と nextState で,これらを基準に true or false を 返却すればよいです. true を返却した時は,再レンダリングが行われます. false を返却した時は,再レンダリングが行われません.

なので,次のようなコードを組めば,再レンダリング回数を絞ることができます

class SampleComponent extends React.Component {
  static get defaultProps() {
    return {
      sampleProp: '',
    }
  }

  constructor(props) {
    super(props);
    this.state = {
      sampleState: '',
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    return !(this.state.sampleState === nextState.sampleState &&
             this.props.sampleProp === this.props.SampleProp);

  }

  render() {
    // 略
  }
}

上記の様にすることで,再レンダリング回数を最小限に絞れます. 具体的には, setState() が 呼びだされても内容の更新がないときは, 再レンダリングされなくなります.

上の例では,単純な文字列しか入っていないケースなので,これでよかったです. しかし,実際に入るstate や props の多くは,Object であったり,Array であることが, 大半です.

Objectが入るならば,Object.is を 使えば良いじゃないかと思いがちですが, 実は罠があります.Object.is は 異なるインスタンスであれば常に false

返してしまします.本来であればこれでいいのですが,ここで求めるのは, Objectの中身,配列の中身が等価かどうかが知りたいはずです. よくある例として JSON.stringfy して比較というものがあります.

これは,個人的に非常にイケてないと考えています. 比較するためだけに文字列に変換している点と,Object が 巨大だとそこそこコストがかかってしまいます.

そこで次の2つを提案します.

lodash の _.isEqual を 用いる

lodash とは何か知りたい方は,当ブログのこの記事を参照していただけると幸いです.
lodash が提供している関数の一つに,_.isEqual というものがあります. これはどのようなものといいますと,

var object = { 'user': 'fred' };
var other = { 'user': 'fred' };

_.isEqual(object, other);
// → true

object === other;
// → false

https://lodash.com/docs#isEqual より引用

のように,Object の中身を正確に比較することができます.もちろん配列も正確に比較できます. 深いObjectもflatten せずに比較することができます. Object の value に関数が入っていても,正しく比較できます. (ただし無名関数の場合は,中身が別でも同じ判定になる)

これを使えば,shouldComponentUpdate() を スマートに実装できます.

  shouldComponentUpdate(nextProps, nextState) {
    const propsDiff = _.isEqual(nextProps, this.props);
    const stateDiff = _.isEqual(nextState, this.state);
    return !(propsDiff && stateDiff);
  }

非常にスマートに実装できました.

Immutable.js の Immutable.is を用いる

Immutable.js とは Facebook が開発した,JavaScript にも Immutable (不変性) を 取り入れるためのモジュールです.

導入するメリットとしては,Reactによく採用されるアーキテクトパターンである, Flux や Redux フレームワークを用いた時に Store の値を view で意図せず書き換えることを防止したり, setState 以外での state の書き換えを抑制するための ライブラリです.

Immutable.js の詳しい内容につきましては,次の記事が分かりやすかったです.

React の開発元も Facebook なので,この2つを組み合わせた時の効果も高く, Facebookも React を用いたアプリケーション開発時に併用を推奨しています.

個人的には,多人数で開発するときは安全のためにも入れておきたいと思っています. 一人で開発してかつstate,store がそこまで大きくない,肥大でないときは, 意図せず書き換えることがないのでなくてもいいかなって思ってます.
これを使うと,上記のようなメリットを得られることも確かなのですが, shouldComponentUpdate の実装も楽になります.

  shouldComponentUpdate(nextProps, nextState) {
    const hogeDiff = Immutable.is(nextProps.hoge, this.props.hoge);
    const pogeDiff = Immutable.is(nextState.poge, this.state.poge);
    return !(propsDiff && stateDiff);
  }

このように書くことができます.メリットとしては,ImmutableなオブジェクトをjsのObjectに変換することなく, 比較できるという点です.( Immutable.js では fromJS で Object を Immutable な Object に, toJS で Immutable な Object を 普通の Object に変換します.そしてこれらは,そこそこコストが高いです.なので変換の必要がないのがメリットとなりえます)


デメリットとしては,_.isEqual とくらべて,比較したいObjectが複数あると まとめて記述することができず,state, props の種類だけいちいちImmutable.is を 書く必要があります.また,比較対象は必ず,Immutable.fromJS or Immutable.Map or Immutable.List などを使って必ず,Immutable化されている必要があります.

Immutable.is([], []) //=> false

いちいち 全Component に実装するのがめんどくさい

いくら,パフォーマンスの改善とはいえ,全Componentに似たような, shouldComponentUpdate を 実装するのは非常にめんどくさいです.

そこで,react の addon である pure-render-mixin の導入を検討するのはいかがでしょうか.

例えば,react-addons-pure-render-mixin を用いる

props, およびstateのプロパティがすべて値型であれば

import PureRenderMixin from 'react-addons-pure-render-mixin';
class FooComponent extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }

  render() {
    return <div className={this.props.className}>foo</div>;
  }
}

とするだけで,再レンダリング回数を最小限にしてくれます. これのやってることは,先ほど上げた,

  shouldComponentUpdate(nextProps, nextState) {
    return !(this.state.sampleState === nextState.sampleState &&
             this.props.sampleProp === this.props.SampleProp);

  }

をすべての props, state に自動で適応してくれるというものです. ただし,値型のものにしか使えませんので,その点は注意してください.

例えば,BaseClass を自分で定義する

汎用的な,shouldComponentUpdate のみを実装した,BaseClass を作成し, React.Component を 継承する代わりに,自作したBaseClass を継承します.

export default class BaseComponent extends React.Component {
  shouldComponentUpdate() {
    const propsDiff = _.isEqual(nextProps, this.props);
    const stateDiff = _.isEqual(nextState, this.state);
    return !(propsDiff && stateDiff);
  }
}
class SapmleComponent extends BaseComponent {
  // 略
}

こうすることで,全てのComponentにいちいちshouldComponentUpdateを実装しなくても,継承するだけでよくなります.

再レンダリング回数を制限することでどれほどパフォーマンスが改善するのか

すでに,計測していた方がいらっしゃったので,その記事を貼っておきます

http://qiita.com/wordijp/items/cfda7bbad195eec22cc3

記事内のグラフを参照していただくとわかると思いますが,汎用的なshouldCompoentUpdate を 実装するだけで,半分ほど再レンダリングコストを抑えることができていると思います.

この記事では,パフォーマンス以外にも汎用的なshouldComponentUpdate の実装方法が 載せられていますが,state と props 以外に this に直接プロパティを埋め込んでいるので 私は,この実装方法をおすすめしません.

利用されなくなった Component のメモリが開放されるときに,開放されることが保証されているのは, props と state のみだからです.

まとめ

  • 再レンダリングされる場面は2種類(追加削除,更新)
  • 追加削除の制御は key を指定するだけで良い
  • 更新を制御するには,shouldComponentUpdate を実装する必要がある.
  • shouldComponentUpdate の実装はそんなに難しくない.
  • shouldComponentUpdate を実装することによって得られる効果は意外と大きい.