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


## はじめに

[ReactNativeにおけるパフォーマンスチューニング（計測編）](https://qiita.com/muraikenta/items/21fad68c184cdc63250e)
の続きです。

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

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

## ReactのDOM更新アルゴリズム
Reactでは、DOMの状態をあるフォーマットのJSオブジェクトとして保持しています。

例えば、

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

のようなDOMの状態を

```javascript
{ 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の計算も軽いわけではないので
ここをなんとか効率化できないか、というのが今回のテーマであります。

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

```javascript
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()`を走らせてみると、
<img width="198" alt="スクリーンショット 2017-12-17 21.02.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/21920/22068686-be4e-7273-65c8-bb1d5a6885ae.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`というメッセージが表示されないようになります。

```javascript
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`をさらに汎用的に実装すると、以下のようになります。

```javascript
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`を実装する必要がなくなります。

```js

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`が出力されてしまいます。

```js
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`であることを鑑みると、必ず`shouldComponentUpdate`は`true`となり、一見同じpropsを渡しているように見えて、異なるpropsとみなされてしまうわけですね。

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

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

const childStyle = {color: 'red'}
```

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

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

```js

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

const childStyle = {color: 'red'}
```

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

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

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


ちなみに`PureComponent`は[Immutable.js](https://facebook.github.io/immutable-js/) と非常に相性がよく、バグを踏み抜きにくくなるので、興味ある人はお試しください！