React製のSPAのパフォーマンスチューニング実例を読んでいて、react-virtualizedというライブラリを試してみたくなりました。 結果としては、初回アクセスのレンダリング速度が4倍ほど速くなりました。
react-virtualizedの概要
Reactコンポーネントがたくさん組み込まれたページをレンダリングしようとすると、コンポーネントの数に比例してレンダリングに時間がかかってしまいます。コンポーネントが少なければ問題はないのですが、スクロールを駆使するようなWebサイトではレンダリングがボトルネックになります。react-virtualizedを使うと、 ブラウザの画面に表示されている領域に存在するコンポーネントのみをレンダリング してくれます。
GitHub
https://github.com/bvaughn/react-virtualized
検証のため作ったアプリ
自分の手元にはボトルネックになるほどのコンポーネント数になるような題材がなかったので、ポケモンを題材にアプリを作ってみました。最近のポケモンは全部で 809匹 もいるようなので、それだけの数のコンポーネントがあれば、react-virtualizedの検証ができると思いました。
- ポケモン全809匹を表示する
- 名前の検索ができる
- タイプで検索ができる
デモサイトを用意してみました。
結果
FMPの数値
react-virtualizedを使うと、初回アクセス時のレンダリング速度が上がりました。
- react-virtualizedを使わない場合
- 3449.4ms
- react-virtualizedを使った場合
- 799.8ms
画面キャプチャ
画面キャプチャも取ってみました。1/4も時間短縮していると、目に見えて違いがわかる感じです。
react-virtualizedを使わない場合
リンク
キャッシュクリアしてからロードしてます。描画に3.5秒くらいかかっています。
react-virtualizedを使った場合
リンク
こちらは1秒以内で初回レンダリングが完了しています。
ちなみに、使ってない版はすべてのポケモンコンポーネントを描画するので、サイトを開いてからずーっと画像取得のリクエストを飛ばしています。一方、react-virtualizedを使うと、初回に生成されるポケモンコンポーネントはごくわずかなので、画像取得のリクエストは少ないです。ただし、スクロールをはじめると、画像リクエストが発生します。
ソースコード
react-virtualizedには様々な機能がありますが、今回使用したのはWindowScrollerというコンポーネントです。
https://bvaughn.github.io/react-virtualized/#/components/WindowScroller
WindowScrollerを使って、Material-UIのCardをグリッド表示します。react-virtualized自体にはリスト表示する機能はありますが、グリッド表示する機能はないです。なので1行に複数のコンポーネントを組み込む計算処理を自前で実装する必要があります。
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>
);
}
トラブったところ
最初は上手く動いていたように見えたのですが、1つ問題がありました。スクロールを行っている途中で横スクロールバーが表示・非表示を繰り返すため、画面がちらつきました。これは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