こんにちは!ぬこすけと申します!
Reactを使って個人で本のおすすめ度を点数化して紹介するサイトを作っています。
ひたすら本のリストページのパフォーマンスチューニングをしていたのですが、LightHouseのパフォーマンススコアで下記のように爆上げすることができました!
-
Before
パフォーマンススコア:30前後くらい
※表示する本のリストは10件 -
After
パフォーマンススコア:90前後くらい
※表示する本のリストは400件程度
今回の開発で得た経験を元に、Reactにおけるパフォーマンスチューニングのノウハウをご紹介したいと思います!
なお、導入効果と導入工数、おすすめ度を主観で5段階でつけさせていただきました!
なお、React Hooksをはじめ、2020/7/28にリリースされたNextJs 9.5など、最近の技術も活用しているので、そこらへんの技術も参考になるのではないかと思います!
※紹介する施策でLight Houseのパフォーマンスがどれくらい上がったか記載していますが、あくまでも私のサイトでの結果です。各々の開発環境によってはまた効果も変わってきますので数値はご参考程度に。
①SPAからSSG化
おすすめ度:★★★★★
導入効果:★★★★★
導入工数:★☆☆☆☆
※導入工数は★が多いほど工数がかからないという意味です。
特に理由がないのであればSSG(静的サイトジェネレーター)化はやるべきです。
これをやるだけで、パフォーマンススコアが30→80くらいまで上がりました(本の表示件数は10件)。
パフォーマンスの観点で言うと、自分のサイトで開発していて感じたSSGのメリットは次の3点あると思います。
- 初期描画の速度が速くなる。
- データの取得が速くなる。
- (NextJSやGatsubyJsなど)デフォルトでサイトを速くする機能が備わっている。
1の「初期描画の速度が早くなる」ことに関して、SPAの場合は初回のスクリプト読み込みに時間がかかりますが、SSGでは初期表示は静的ファイルとして読み込むので、SPAに比べてはるかに初期描画が速いです。
2については、Webサイトに表示するデータは予めJsonでビルドされるので、都度APIにリクエスト投げてDBからデータを取得し、レスポンスを返すというようなことがなくなります。
私のサイトの例でいうと、書籍のデータを提供するAPIサーバーのマシンスペックがしょぼい、地理的に日本から遠いなどの理由で、APIサーバー自体のレスポンスが遅いことが問題としてありました。
またSPAのシステム構成だとプロキシサーバーをはさむ必要があったので、どうしてもフロント側からデータ取得が遅いこともネックでした。
SSGにすることによって、データは事前にJson形式でビルド、初回アクセスはHtml、追加のデータ取得はプロキシサーバーを使わず直接アプリケーションをビルドしているホスティングサーバーとのやりとりのみに抑えることができました。
一方の3について、SSGを提供しているフレームワークには様々なパフォーマンスを上げる機能が備わっています。
私のサイトではNextJsを使っていますが、例えばリンク先のページについてバックグラウンドでpreloadする機能があったり、CSSを自動でミニファイ化・分割する機能もあります。
余談ですが、APIサーバーへのアクセスを減らせるというのもメリットの一つです。
私のサイトではAPIサーバーはクラウドサービスを使っています。今はアクセス数も貧弱なので無料枠で運用できていますが、将来的にアクセスが多くなると課金が発生してしまいます。SSGにすればビルド時のみのアクセスに抑えることも可能です。
メリットとしては上記が挙げられますが、ReactでのSSG導入はどうすれば良いのでしょうか?
ReactでSSG対応するならNextJsかGatsubyJsの2択ですが、個人的には導入のハードルが低いNextJsがおすすめです。
NextJsはGatsubyJsと比べて学習コストが低いこと(GatsubyJsはGraphQLを勉強する必要があります)や、SSRとのハイブリッドも実現可能です。
さらに2020/7/28には、**動的にビルドする機能もリリース**されました。SSGのデメリットとしては、「データを更新するたびにフロント側もビルドしなくてはならない」ことが挙げられますが、NextJsは見事にこの問題へ対応しています。
導入工数について、NextJsに限った話ではありますが、SSG機能自体がかなり最近のものでドキュメントが少なく、問題が起きた時はgithubのissueを英語で読まなければならない場合があります。また、NextJsを開発しているVercelも仕様については手探り感があり、仕様も「え?これできないの?」みたいなこともあってハマることもあるので、学習コストは低いものの導入工数は高めかもしれません。
②CSSフレームワークを剥がしてフルスクラッチに
おすすめ度:★★★★☆
導入効果:★★★★☆
導入工数:★★☆☆☆
この対応ではパフォーマンススコアが20くらい上がりました。
元々自分のサイトではCSSフレームワークとしてmaterial-uiを使っていました。
Chromeのdeveloper toolsでボトルネック調査をしていたところ、material-uiがボトルネックということがわかりました。具体的には、コアライブラリであるReactと同レベルで読み込みに時間がかかっていました。
※ボトルネットの調査の方法は下記のサイトが参考になります。
React製のSPAのパフォーマンスチューニング実例
なので、material-uiを使うのをやめて、1からスタイルを当て直す対応をしました。
具体的には、NextJsの機能を使ってCSS Modulesを利用しました。
Reactでスタイルを当てる方法としてはいくつかありますが、次の2つについてはパフォーマンスの観点からあまりおすすめしません。
- インラインスタイル
- CSS in JS
1のインラインスタイルに関しては、Reactの公式ドキュメントでも言及されています。
パフォーマンス観点から言えば、基本的に CSS クラスを使う方が、インラインスタイルを用いるよりも優れています。
CSS とスタイルの使用
2については、styled-componentsに代表されるようなCSS in JSもパフォーマンスが落ちる可能性があります。
(興味があれば下記の記事をご参考ください)
パフォーマンスとはまた話が逸れますが、NextJsではCSS in JSはSSRに対応していないので、もしSSR対応をしなくてはならない場合を考慮してCSS in JSを使わない方が無難です。
アプリケーションの規模感によりますが、CSSフレームワークからフルスクラッチに移行するのもかなり工数かかると思います。
(ちなみに私の場合は3日くらいでした)
③画面に表示されているコンポーネントのみレンダリング
おすすめ度:★★★☆☆
導入効果:★★★★☆
導入工数:★★☆☆☆
この対応でもパフォーマンススコアが20くらい上がりました。
リストの件数が少なければ問題ないのですが、数百件数千件というレベルになるとレンダリングにも時間がかかります。
この問題に対処するためにはどうしたら良いか?親切にもReactの公式ドキュメントで紹介されています。
アプリケーションが長いデータのリスト(数百〜数千行)をレンダーする場合は、「ウィンドウイング」として知られるテクニックを使うことをおすすめします。このテクニックでは、ある瞬間ごとにはリストの小さな部分集合のみを描画することで、生成する DOM ノードの数およびコンポーネントの再描画にかかる時間を大幅に削減することができます。
パフォーマンス最適化
この「ウィンドウイング」について、海外の色々なサイトを調査したところ、「react-window」か「react-virtualized」が主流のようです。
(このライブラリについても上記のReactの公式サイトに紹介されています)
「react-window]と「react-virtualized」のどちらを使うべきかですが、特段理由がなければreact-windowを使うべきです。
実は両者とも同じ人が開発しているのですが、下記のページで記載されている通りできるだけreact-windowを使うように勧めています。
また、react-virtualizedよりもreact-windowの方が軽量です。
2020/8/13時点の最新バージョンで、react-virtualized(ver9.22.2)は2.27Mb, react-window(ver1.8.5)は865Kbです。
react-windowですが、結構使い方に癖があったり、日本語ドキュメントも少ないので導入は少し骨が折れるかもしれません。
また別途SEO上の懸念もあります。
無限スクロールはGoogleに評価されづらい問題や、DOM上では画面外のコンポーネントは存在しないのでGoogleに適性に評価されないのでは?という問題があります。
(例えば、100件リストがあったとしてもDOM上では3件しか存在しないので、100件あるコンテンツが3件しかないとGoogleに認識される可能性はあるかもしれません)。
④React.memoやuseMemo, useCallbackを駆使してメモ化
おすすめ度:★★☆☆☆
導入効果:???
導入工数:★★★★★
正直にいいます。効果測定していません笑
ただ、知っといて絶対に損はないというのと、工数もそんなにかからないと思うのでご紹介します。
※ここからはReact-hooksの話になります。
前提として知っておかなければならないのは、Reactのレンダリングの仕組みです。
親コンポーネントの状態が変わると、問答無用で子コンポーネントも再レンダリングされます。
子コンポーネントが親コンポーネントの状態に依存するものであれば問題ないのですが、
逆に子が親に依存しないものの場合、無駄に再レンダリングされてしまいます。
また、子コンポーネントで複雑な計算(数千件の配列のソートなど)をしていると、再レンダリングされる度に時間のかかる計算が走ります。
ぬこぷろのサイトを例にあげます。
下のキャプチャをご覧ください。
ぬこぷろではヘッダーにハンバーガーメニューが設置してあり、押下するとサイドメニューが開きます。
このサイドメニューには「開いている/開いていない」という状態を持っていますが、何も対策をしていないとサイドメニューを開くたびにメニュー上のテキストも再レンダリングされます。
また、このサイトでは複雑な計算はしていませんが、もしサイドメニューのリストが数百件あって、配列のソートなりデータの整形などしていると、レンダリングのたびに再計算が走ってしまいます。
ではどうしたら良いのでしょうか。やることは次の3点です。
- React.memoでコンポーネント自体をメモ化し、再レンダリング防止
- 子コンポーネントにコールバック関数を渡す場合は、useCallbackでメモ化し、子コンポーネントの再レンダリングを防止
- コンポーネント内の計算はuseMemoでメモ化し、再レンダリングが発生しても再計算は防止
先ほどのサイドメニューの例で言うと、リストなどは基本的にReact.memoでメモ化しています。
また、「サイドメニューのリンクを押下したらサイドメニューを閉じる」ことを実現するために、useCallbackを利用して子コンポーネントに関数を渡すことで、子コンポーネントの再レンダリングを極力防止しています。
また、リストでの整形処理もuseMemoを使っています。
上記のReact.memoやuseMemoなどの具体的な使い方は、ググると色々な記事で紹介されているので、この記事では割愛します。
手軽に導入はできるので興味ある方はぜひやってみてください!
まとめ
この記事ではReactアプリケーションのパフォーマンスチューニングの方法について、次の4つをご紹介しました。
- SPAからSSG化
- CSSフレームワークを剥がしてフルスクラッチに
- 画面に表示されているコンポーネントのみレンダリング
- React.memoやuseMemo, useCallbackを駆使してメモ化
サイトの表示速度の重要性は年々増してきており、Googleが2021年以降にSEOのランキング要因に組み込むと言われているWeb Vitalにも表示速度が指標として入っています。
ReactでWebアプリを作っている方へのご参考になればと思います!