【2020】Reactパフォーマンスチューニング  ~LightHouse Score 爆上げ物語~

こんにちは！ぬこすけと申します！
Reactを使って個人で[本のおすすめ度を点数化して紹介するサイト](https://nuko-programming.com/ "ぬこぷろ")を作っています。
ひたすら[本のリストページ](https://nuko-programming.com/ranking/Python "初心者向けPythonのおすすめ入門書ランキング")のパフォーマンスチューニングをしていたのですが、**LightHouseのパフォーマンススコアで下記のように爆上げ**することができました！

* Before
パフォーマンススコア：30前後くらい
※表示する本のリストは10件

* After
パフォーマンススコア：**90**前後くらい
※表示する本のリストは400件程度

![スクリーンショット 2020-08-12 23.34.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/3bed7edd-beff-8f03-9381-0a2a40c5900e.png)
(2020/8/12に計測)

今回の開発で得た経験を元に、Reactにおけるパフォーマンスチューニングのノウハウをご紹介したいと思います！
なお、導入効果と導入工数、おすすめ度を主観で５段階でつけさせていただきました！

なお、React Hooksをはじめ、2020/7/28にリリースされた[NextJs 9.5](https://nextjs.org/blog/next-9-5)など、**最近の技術も活用しているので、そこらへんの技術も参考になるのではないか**と思います！

※紹介する施策でLight Houseのパフォーマンスがどれくらい上がったか記載していますが、あくまでも[私のサイト](https://nuko-programming.com/ "ぬこぷろ")での結果です。各々の開発環境によってはまた効果も変わってきますので数値はご参考程度に。

# ①SPAからSSG化
おすすめ度：★★★★★
導入効果：★★★★★
導入工数：★☆☆☆☆
※導入工数は★が多いほど工数がかからないという意味です。

特に理由がないのであれば**SSG（静的サイトジェネレーター）化はやるべき**です。
これをやるだけで、**パフォーマンススコアが30→80くらいまで上がりました**（本の表示件数は10件）。

パフォーマンスの観点で言うと、[自分のサイト](https://nuko-programming.com/ "ぬこぷろ")で開発していて感じた**SSGのメリットは次の３点**あると思います。

1. 初期描画の速度が速くなる。
2. データの取得が速くなる。
3. (NextJSやGatsubyJsなど)デフォルトでサイトを速くする機能が備わっている。

1の「初期描画の速度が早くなる」ことに関して、SPAの場合は初回のスクリプト読み込みに時間がかかりますが、SSGでは**初期表示は静的ファイルとして読み込むので、SPAに比べてはるかに初期描画が速い**です。

２については、**Webサイトに表示するデータは予めJsonでビルドされる**ので、都度APIにリクエスト投げてDBからデータを取得し、レスポンスを返すというようなことがなくなります。
[私のサイト](https://nuko-programming.com/ "ぬこぷろ")の例でいうと、書籍のデータを提供するAPIサーバーのマシンスペックがしょぼい、地理的に日本から遠いなどの理由で、APIサーバー自体のレスポンスが遅いことが問題としてありました。

またSPAのシステム構成だとプロキシサーバーをはさむ必要があったので、どうしてもフロント側からデータ取得が遅いこともネックでした。
SSGにすることによって、データは事前にJson形式でビルド、初回アクセスはHtml、追加のデータ取得はプロキシサーバーを使わず直接アプリケーションをビルドしているホスティングサーバーとのやりとりのみに抑えることができました。

一方の3について、**SSGを提供しているフレームワークには様々なパフォーマンスを上げる機能が備わっています**。
[私のサイト](https://nuko-programming.com/ "ぬこぷろ")ではNextJsを使っていますが、例えばリンク先のページについてバックグラウンドでpreloadする機能があったり、CSSを自動でミニファイ化・分割する機能もあります。

余談ですが、APIサーバーへのアクセスを減らせるというのもメリットの一つです。
[私のサイト](https://nuko-programming.com/ "ぬこぷろ")ではAPIサーバーはクラウドサービスを使っています。今はアクセス数も貧弱なので無料枠で運用できていますが、将来的にアクセスが多くなると課金が発生してしまいます。SSGにすればビルド時のみのアクセスに抑えることも可能です。

メリットとしては上記が挙げられますが、ReactでのSSG導入はどうすれば良いのでしょうか？

ReactでSSG対応するならNextJsかGatsubyJsの２択ですが、個人的には導入のハードルが低いNextJsがおすすめです。

NextJsはGatsubyJsと比べて**学習コストが低い**こと(GatsubyJsはGraphQLを勉強する必要があります)や、**SSRとのハイブリッドも実現可能**です。

さらに2020/7/28には、**[動的にビルドする機能もリリース](https://nextjs.org/blog/next-9-5)**されました。SSGのデメリットとしては、「データを更新するたびにフロント側もビルドしなくてはならない」ことが挙げられますが、NextJsは見事にこの問題へ対応しています。

導入工数について、NextJsに限った話ではありますが、SSG機能自体がかなり最近のものでドキュメントが少なく、問題が起きた時はgithubのissueを英語で読まなければならない場合があります。また、NextJsを開発しているVercelも仕様については手探り感があり、仕様も「え？これできないの？」みたいなこともあってハマることもあるので、学習コストは低いものの**導入工数は高め**かもしれません。

# ②CSSフレームワークを剥がしてフルスクラッチに
おすすめ度：★★★★☆
導入効果：★★★★☆
導入工数：★★☆☆☆

この対応では**パフォーマンススコアが20くらい上がりました**。

元々[自分のサイト](https://nuko-programming.com/ "ぬこぷろ")ではCSSフレームワークとして[material-ui](https://material-ui.com/ "")を使っていました。

Chromeのdeveloper toolsでボトルネック調査をしていたところ、material-uiがボトルネックということがわかりました。具体的には、コアライブラリであるReactと同レベルで読み込みに時間がかかっていました。

※ボトルネットの調査の方法は下記のサイトが参考になります。
[React製のSPAのパフォーマンスチューニング実例](https://recruit-tech.co.jp/blog/2018/09/19/react_spa_performance_tuning/, "React製のSPAのパフォーマンスチューニング実例")

なので、material-uiを使うのをやめて、１からスタイルを当て直す対応をしました。
具体的には、NextJsの機能を使って[CSS Modules](https://nextjs.org/docs/basic-features/built-in-css-support)を利用しました。

Reactでスタイルを当てる方法としてはいくつかありますが、**次の２つについてはパフォーマンスの観点からあまりおすすめしません。**

1. インラインスタイル
2. CSS in JS

1のインラインスタイルに関しては、Reactの公式ドキュメントでも言及されています。
>パフォーマンス観点から言えば、基本的に CSS クラスを使う方が、インラインスタイルを用いるよりも優れています。
[CSS とスタイルの使用](https://ja.reactjs.org/docs/faq-styling.html)

2については、styled-componentsに代表されるような**CSS in JSもパフォーマンスが落ちる可能性**があります。
（興味があれば下記の記事をご参考ください）

* [CSS-in-JSパフォーマンスコスト - 緩和戦略](https://www.infoq.com/jp/news/2020/01/css-cssinjs-performance-cost/)
* [Webサイト開発にCSS in JavaScriptを使うのはやめよう](https://postd.cc/stop-using-css-in-javascript-for-web-development-fa/)

パフォーマンスとはまた話が逸れますが、NextJsではCSS in JSはSSRに対応していないので、もしSSR対応をしなくてはならない場合を考慮してCSS in JSを使わない方が無難です。

アプリケーションの規模感によりますが、**CSSフレームワークからフルスクラッチに移行するのもかなり工数かかる**と思います。
（ちなみに私の場合は3日くらいでした）

# ③画面に表示されているコンポーネントのみレンダリング
おすすめ度：★★★☆☆
導入効果：★★★★☆
導入工数：★★☆☆☆

この対応でも**パフォーマンススコアが20くらい上がりました**。

リストの件数が少なければ問題ないのですが、数百件数千件というレベルになるとレンダリングにも時間がかかります。
この問題に対処するためにはどうしたら良いか？親切にもReactの公式ドキュメントで紹介されています。

>アプリケーションが長いデータのリスト（数百〜数千行）をレンダーする場合は、「ウィンドウイング」として知られるテクニックを使うことをおすすめします。このテクニックでは、ある瞬間ごとにはリストの小さな部分集合のみを描画することで、生成する DOM ノードの数およびコンポーネントの再描画にかかる時間を大幅に削減することができます。
[パフォーマンス最適化](https://ja.reactjs.org/docs/optimizing-performance.html)

この「ウィンドウイング」について、海外の色々なサイトを調査したところ、「react-window」か「react-virtualized」が主流のようです。
(このライブラリについても上記のReactの公式サイトに紹介されています)

「react-window]と「react-virtualized」のどちらを使うべきかですが、特段理由がなければ**react-window**を使うべきです。
実は両者とも同じ人が開発しているのですが、下記のページで記載されている通りできるだけreact-windowを使うように勧めています。

* [react-windowのgithub](https://github.com/bvaughn/react-window#how-is-react-window-different-from-react-virtualized)

また、**react-virtualizedよりもreact-windowの方が軽量**です。
2020/8/13時点の最新バージョンで、react-virtualized(ver9.22.2)は2.27Mb, react-window(ver1.8.5)は865Kbです。

* [npm react-window](https://www.npmjs.com/package/react-window)
* [npm react-virtualized](https://www.npmjs.com/package/react-virtualized)

react-windowですが、**結構使い方に癖があったり、日本語ドキュメントも少ないので導入は少し骨が折れる**かもしれません。

また別途**SEO上の懸念**もあります。
無限スクロールはGoogleに評価されづらい問題や、DOM上では画面外のコンポーネントは存在しないのでGoogleに適性に評価されないのでは？という問題があります。
(例えば、100件リストがあったとしてもDOM上では3件しか存在しないので、100件あるコンテンツが3件しかないとGoogleに認識される可能性はあるかもしれません)。

# ④React.memoやuseMemo, useCallbackを駆使してメモ化
おすすめ度:★★☆☆☆
導入効果：？？？
導入工数：★★★★★

正直にいいます。効果測定していません笑
ただ、知っといて絶対に損はないというのと、工数もそんなにかからないと思うのでご紹介します。
※ここからはReact-hooksの話になります。

前提として知っておかなければならないのは、Reactのレンダリングの仕組みです。
**親コンポーネントの状態が変わると、問答無用で子コンポーネントも再レンダリング**されます。

子コンポーネントが親コンポーネントの状態に依存するものであれば問題ないのですが、
逆に子が親に依存しないものの場合、無駄に再レンダリングされてしまいます。
また、子コンポーネントで複雑な計算（数千件の配列のソートなど）をしていると、再レンダリングされる度に時間のかかる計算が走ります。

[ぬこぷろのサイト](https://nuko-programming.com/ranking/Python "初心者向けPythonのおすすめ入門書ランキング")を例にあげます。
下のキャプチャをご覧ください。

![スクリーンショット 2020-08-13 10.02.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/a3c709ec-b736-53d8-ff0f-6e74469c6746.png)

ぬこぷろではヘッダーにハンバーガーメニューが設置してあり、押下するとサイドメニューが開きます。
このサイドメニューには「開いている/開いていない」という状態を持っていますが、何も対策をしていないとサイドメニューを開くたびにメニュー上のテキストも再レンダリングされます。
また、このサイトでは複雑な計算はしていませんが、もしサイドメニューのリストが数百件あって、配列のソートなりデータの整形などしていると、レンダリングのたびに再計算が走ってしまいます。

ではどうしたら良いのでしょうか。やることは次の3点です。

1. React.memoでコンポーネント自体をメモ化し、再レンダリング防止
2. 子コンポーネントにコールバック関数を渡す場合は、useCallbackでメモ化し、子コンポーネントの再レンダリングを防止
3. コンポーネント内の計算はuseMemoでメモ化し、再レンダリングが発生しても再計算は防止

先ほどのサイドメニューの例で言うと、リストなどは基本的にReact.memoでメモ化しています。
また、「サイドメニューのリンクを押下したらサイドメニューを閉じる」ことを実現するために、useCallbackを利用して子コンポーネントに関数を渡すことで、子コンポーネントの再レンダリングを極力防止しています。
また、リストでの整形処理もuseMemoを使っています。

上記のReact.memoやuseMemoなどの具体的な使い方は、ググると色々な記事で紹介されているので、この記事では割愛します。
**手軽に導入はできる**ので興味ある方はぜひやってみてください！

# まとめ
この記事ではReactアプリケーションのパフォーマンスチューニングの方法について、次の４つをご紹介しました。

1. SPAからSSG化
2. CSSフレームワークを剥がしてフルスクラッチに
3. 画面に表示されているコンポーネントのみレンダリング
4. React.memoやuseMemo, useCallbackを駆使してメモ化

サイトの表示速度の重要性は年々増してきており、Googleが2021年以降にSEOのランキング要因に組み込むと言われている[Web Vital](https://webtan.impress.co.jp/e/2020/06/05/36210)にも表示速度が指標として入っています。
ReactでWebアプリを作っている方へのご参考になればと思います！