React-Virtualizedでレンダリングを効率化する

[React製のSPAのパフォーマンスチューニング実例](https://recruit-tech.co.jp/blog/2018/09/19/react_spa_performance_tuning/)を読んでいて、[react-virtualized](https://github.com/bvaughn/react-virtualized)というライブラリを試してみたくなりました。 **結果としては、初回アクセスのレンダリング速度が4倍ほど速くなりました。**

# react-virtualizedの概要
Reactコンポーネントがたくさん組み込まれたページをレンダリングしようとすると、コンポーネントの数に比例してレンダリングに時間がかかってしまいます。コンポーネントが少なければ問題はないのですが、スクロールを駆使するようなWebサイトではレンダリングがボトルネックになります。react-virtualizedを使うと、 **ブラウザの画面に表示されている領域に存在するコンポーネントのみをレンダリング** してくれます。

**GitHub**
https://github.com/bvaughn/react-virtualized

# 検証のため作ったアプリ
自分の手元にはボトルネックになるほどのコンポーネント数になるような題材がなかったので、ポケモンを題材にアプリを作ってみました。最近のポケモンは全部で **809匹** もいるようなので、それだけの数のコンポーネントがあれば、react-virtualizedの検証ができると思いました。

* ポケモン全809匹を表示する
* 名前の検索ができる
* タイプで検索ができる

[デモサイト](https://pokemon-react-virtualized.firebaseapp.com/)を用意してみました。

<img width="500" alt="ポケモン一覧" src="https://qiita-image-store.s3.amazonaws.com/0/10499/ed8821b4-db18-fea6-ee9c-095fcc0f3b48.png">

# 結果
### FMPの数値
react-virtualizedを使うと、初回アクセス時のレンダリング速度が上がりました。

* react-virtualizedを使わない場合
    * 3449.4ms
* react-virtualizedを使った場合
    * 799.8ms

### 画面キャプチャ
画面キャプチャも取ってみました。1/4も時間短縮していると、目に見えて違いがわかる感じです。

**react-virtualizedを使わない場合**
[リンク](https://pokemon-react-virtualized.firebaseapp.com/normal)
キャッシュクリアしてからロードしてます。描画に3.5秒くらいかかっています。
![before.gif](https://qiita-image-store.s3.amazonaws.com/0/10499/2f40017c-c310-4644-2fd8-1fd2753b533f.gif)

**react-virtualizedを使った場合**
[リンク](https://pokemon-react-virtualized.firebaseapp.com/)
こちらは1秒以内で初回レンダリングが完了しています。
![after.gif](https://qiita-image-store.s3.amazonaws.com/0/10499/dea91cb4-0d4a-bc25-a37d-938b78a2388b.gif)

ちなみに、使ってない版はすべてのポケモンコンポーネントを描画するので、サイトを開いてからずーっと画像取得のリクエストを飛ばしています。一方、react-virtualizedを使うと、初回に生成されるポケモンコンポーネントはごくわずかなので、画像取得のリクエストは少ないです。ただし、スクロールをはじめると、画像リクエストが発生します。

# ソースコード
react-virtualizedには様々な機能がありますが、今回使用したのはWindowScrollerというコンポーネントです。
https://bvaughn.github.io/react-virtualized/#/components/WindowScroller

WindowScrollerを使って、Material-UIのCardをグリッド表示します。react-virtualized自体にはリスト表示する機能はありますが、グリッド表示する機能はないです。なので1行に複数のコンポーネントを組み込む計算処理を自前で実装する必要があります。

```jsx
render() {
  const { classes, pokemons } = this.props;
  return (
    <div className={classes.root}>
      <WindowScroller>
        {({ width, height, isScrolling, registerChild, scrollTop }) => {
          // 1行あたりに何匹を描画するかwidthから計算
          const itemsPerRow = Math.max(1, Math.floor(width / CARD_WIDTH) - 1);
          // 全体で何行必要か計算
          const rowCount = Math.ceil(pokemons.length / itemsPerRow);

          return (
            <React.Fragment>
              <div ref={registerChild} className={classes.cardArea}>
                <List
                  autoHeight
                  width={width}
                  height={height}
                  isScrolling={isScrolling}
                  scrollTop={scrollTop}
                  rowCount={rowCount}
                  rowHeight={CARD_HEIGHT + ROW_HEIGHT_MARGIN}
                  rowRenderer={({ index, key, style }) => {
                    const items = []; // 1行に表示するコンポーネントを格納するリスト
                    // from toを計算して、itemsへコンポーネントをappendする
                    const fromIndex = index * itemsPerRow;
                    const toIndex = Math.min(fromIndex + itemsPerRow, pokemons.length);
                    for (let i = fromIndex; i < toIndex; i++) {
                      items.push(<PokeCard key={i} pokemon={pokemons[i]} />);
                    }
                    // グリッドの最終行を左寄せにするための処理。何も表示しないコンポーネントをappend
                    const emptySize = itemsPerRow - items.length;
                    for (let i = 0; i < emptySize; i++) {
                      items.push(<PokeCard key={i + toIndex} empty />);
                    }
                    return (
                      <div className={classes.row} key={key} style={style}>
                        {items}
                      </div>
                    );
                  }}
                />
              </div>
            </React.Fragment>
          );
        }}
      </WindowScroller>
    </div>
  );
}
```

# トラブったところ
最初は上手く動いていたように見えたのですが、１つ問題がありました。スクロールを行っている途中で横スクロールバーが表示・非表示を繰り返すため、画面がちらつきました。これはissueにも上がっていました。
https://github.com/bvaughn/react-virtualized/issues/955

なので暫定対応として、横スクロールを常にonにするcssを追記することにしました。

# ソースコード
https://github.com/ksakiyama/pokemon-react-virtualized
もっときれいに書ける方法があれば教えてください。

# まとめ
* react-virtualizedを使うと、画面に表示されないエリアのコンポーネント描画をしなくて良い
    * 初回のレンダリング速度が上がる。使わない場合と比べて1/4の速度改善
    * スクロールして画面表示される直前のタイミングでDOM生成して、効率的
* react-virtualizedにはグリッド表示の機能はないので、自前で計算処理をする必要がある

# 参考
**react-virtualized**
[AirNYT: React-Virtualized + Material-UI Cards for Fast Lists](https://appendto.com/2018/11/airnyt-react-virtualized-material-ui-cards-for-fast-lists/)

**ポケモン**
[alik0211/pokedex](https://github.com/alik0211/pokedex)
[Pokemon.json](https://github.com/fanzeyi/pokemon.json)
[すばらしきポケモンエコシステム⚡️](https://qiita.com/seya/items/47dc0ebae55674d8902f)