43
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Organization

Reactにおけるパフォーマンスチューニング(改善編)

はじめに

ReactNativeにおけるパフォーマンスチューニング(計測編)
の続きです。

続きと言いつつも、今回はただのReactの話になってしまいましたので、
WebでReactを書いている人も参考にしていただければと思います。

Reactのパフォーマンス改善に関しては公式docsにめちゃくちゃいい記事があります。
なのでこれを読めば十分なのですが、英語っていうこともあるので自分なりに噛み砕いてざっくり要点を説明してみます。

ReactのDOM更新アルゴリズム

Reactでは、DOMの状態をあるフォーマットのJSオブジェクトとして保持しています。

例えば、

<div className="hoge">some text</div>

のようなDOMの状態を

{ type: 'div', props: {className: 'hoge'}, children: ['some text'] }

のようなJSオブジェクトとして保持しています。

このDOMを抽象化したJSオブジェクトのことをVirtual DOM と呼びます。

Reactでは、DOMの更新をする際にこのVirtual DOMに差分が生じた場合のみ実際にレンダリングし直すことによって、DOM更新を効率化しています。

更新の際に、実際にDOM同士を比較するよりも遥かにJSオブジェクトの比較の方が高速のため、このような工夫をしているわけです。

また、あるコンポーネントが更新される際、全ての子孫コンポーネントにおいてもVirtual DOMの差分計算が走ります。

ここが素晴らしくもあり厄介なところです。
ある程度大規模なプロダクトだと一画面を描画するのに数百 ~ 数千のコンポーネントがマウントされたりしますよね。

例えば5000個のコンポーネントがマウントされている画面があるとします。
ルートコンポーネントをアップデートすると、その5000個のコンポーネントにおいて、Virtual DOMの差分計算が走るわけです。

Virtual DOMの計算も軽いわけではないので
ここをなんとか効率化できないか、というのが今回のテーマであります。

更新アルゴリズムを最適化する

class Child extends React.Component {
  render() {
    console.log('virtual dom calculated')
    return (<h2>{this.props.text}</h2>)
  }
}

のようなコンポーネントを<Child text='hoge' />のようにマウントしたとしましょう。
textというプロパティは固定です。

親コンポーネントでsetStateなどにより更新が発生した時に、Childコンポーネントのrenderも走ります。
試しに親クラスで100回setState()を走らせてみると、
スクリーンショット 2017-12-17 21.02.33.png
のようにconsole上に出力されます。(つまりvirtual DOMの計算が100回行われています。)

今回のケースで言えば、textプロパティを変えていないのだから、virtual DOMを計算するまでもなく、ビューの更新をしなくてよいことは自明ですよね。

なので、Childコンポーネントのrenderメソッドは100回も実行される必要はなく、ただの1回でよかったはずです。

この事例をより一般化して考えてみましょう。
ReactコンポーネントのDOMは、stateとpropsによって一意に決まってくるはずです。
(stateとprops以外に依存している場合は、そもそも設計を見直したほうがいいかもしれません。)
ならば、stateとpropsが変化していない時は、わざわざvirtual DOMの計算をするまでもなくない?という発想ができるのではないでしょうか。

こういった考えのもと、ReactのコンポーネントにはshouldComponentUpdateといメソッドが用意されています。

shouldComponentUpdateメソッドはrenderメソッドが呼ばれる直前に実行され、このメソッドの返り値をfalseとするとrenderメソッドが走らない(virtual DOMが計算されない)ようになります。

試しに以下のようにshouldComponentUpdateを実装すると、親コンポーネントで100回setState()を走らせても、一度しかvirtual dom calculatedというメッセージが表示されないようになります。

class Child extends React.Component {
  shouldComponentUpdate(nextProps) {
    if (this.props.text !== nextProps.text) {
      return true
    }
    return false
  }

  render() {
    console.log('virtual dom calculated')
    return (<h2>{this.props.text}</h2>)
  }
}

これにて計算コストを1/100に抑えることができました:clap: :clap:

PureComponent

shouldComponentUpdateをさらに汎用的に実装すると、以下のようになります。

shouldComponentUpdate(nextProps, nextState) {
  for (let propKey in this.props) {
    if (this.props[propKey] !== nextProps[propKey]) {
      return true
    }
  }

  for (let stateKey in this.state) {
    if (this.state[stateKey] !== nextState[stateKey]) {
      return true
    }
  }

  return false
}

全てのpropsとstateが同じなら、falseを返して、virtual DOMの計算を行わないようになっています。
Reactではこの実装は一つのパターンであるため、これと同じメソッドを実装済みのReact.PureComponentクラスが存在します。(実際の実装はもう少し複雑です。)

なので、先程のChildクラスをReact.PureComponentクラスを継承するようにすると、わざわざshouldComponentUpdateを実装する必要がなくなります。


class Child extends React.PureComponent {
  render() {
    console.log('virtual dom calculated')
    return (<h2>{this.props.text}</h2>)
  }
}

注意点

PureComponentを使う際にはいくつか注意点があるので、いくつか紹介します。

1. PureComponentのはずなのに、毎回virtual DOMの計算が走ってしまう!

PureComponentであるChildコンポーネントがstyleというオブジェクトをpropsとして持つとしましょう。
親クラスで以下のようにChildをマウントしていると、Parentで更新計算が行われると必ずChildのvirtual dom calculatedが出力されてしまいます。

class Parent extends React.Component {
  ...
  render() {
    return (
      <Child style={{color: 'red'}} text='hello' />
    )
  }
}

一体なぜでしょう?
shouldComponentUpdate()の汎用的な実装を思い出してください。
if (this.props[propKey] !== nextProps[propKey])のようになっています。
そして、今回のケースでは、styleプロパティに毎回新しいオブジェクトを渡してしまっていることに注目してください。
{color: 'red'} !== {color: 'red'}trueであることを鑑みると、必ずshouldComponentUpdatetrueとなり、一見同じpropsを渡しているように見えて、異なるpropsとみなされてしまうわけですね。

今回のようなケースでは、以下のようにクラス外にstyleを定義しておくと、毎回同一のオブジェクトを渡すことになるため、Parentクラスの更新がなされても、Childクラスにてvirtual dom calculatedが出力されることはなくなります。

class Parent extends React.Component {
  ...
  render() {
    return (
      <Child style={childStyle} text='hello' />
    )
  }
}

const childStyle = {color: 'red'}

2. propsを変えたはずなのに、見た目が変わらない!

以下のようなコードを書いた時に、this.state.colorが変化しても、Childコンポーネントの色が全く変わらない。そんな現象が起きます。


class Parent extends React.Component {
  ...
  render() {
    childStyle.color = this.state.color
    return (
      <Child style={childStyle} text='hello' />
    )
  }
}

const childStyle = {color: 'red'}

勘のいい人はお気づきだと思います。
今回のケースでは先程のケースと逆の罠、即ち「オブジェクトの中身を変えてもObjectIdが同じ」であることが原因です。
以下のコードが成り立つことと要は同じですね。

const a = {color: 'red'}
b = a
b.color = 'blue'
a === b // true

最後に

というわけで、React.PureComponentを使えばReactのパフォーマンスを向上させられることがわかりました。
注意点であげたように、PureComponentは思わぬバグを引き起こすことがあることに注意した上で慎重にお使いください。
また、このようなバグの引き起こしやすさから、PureComponentの乱用はおすすめしません。
ReactNativeにおけるパフォーマンスチューニング(計測編)
でも紹介した、Google ChromeのPerformance計測ツールなどを使って、ボトルネックと特定できた段階で使うようにしましょう。

ちなみにPureComponentImmutable.js と非常に相性がよく、バグを踏み抜きにくくなるので、興味ある人はお試しください!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
43
Help us understand the problem. What are the problem?