Reactは、コンポーネントを組み合わせて柔軟なUIを構築できる人気のフロントエンドライブラリです。
一方で、プロジェクトが大きくなると、コンポーネント数や状態管理が増え、不要な再レンダリングや大きなJavaScriptバンドルがパフォーマンス低下の原因になることがあります。
本記事では、Reactアプリケーションを快適に動作させるために意識したい基本的なパフォーマンス改善ポイントを紹介します。
Reactでパフォーマンス問題が起きる主な理由
Reactアプリが重くなる原因は一つではありません。
代表的なものには、
-
不要な再レンダリング
-
大きすぎるコンポーネント
-
重い計算処理
-
大量のリスト表示
-
不要なライブラリ
-
大きなバンドルサイズ
-
過剰な状態管理
などがあります。
まずは「どこが遅いのか」を測定し、原因に合った対策を行うことが重要です。
不要な再レンダリングを減らす
Reactでは、stateやpropsが変化するとコンポーネントが再レンダリングされます。
これは基本的な仕組みですが、必要のないコンポーネントまで再レンダリングされると処理負荷が増えます。
例えば、親コンポーネントのstate変更によって、内容が変わっていない子コンポーネントまで再レンダリングされる場合があります。
こうしたケースでは、コンポーネント構造を見直すことが有効です。
React.memoを適切に使う
React.memoを利用すると、propsが変わっていないコンポーネントの再レンダリングを抑えられる場合があります。
特に、
-
表示内容が複雑
-
再レンダリング回数が多い
-
propsの変化が少ない
コンポーネントで効果があります。
ただし、すべてのコンポーネントへReact.memoを付ければ良いわけではありません。
props比較にもコストがかかるため、実際に問題がある場所で利用することが大切です。
useMemoで重い計算を抑える
コンポーネントのレンダリングごとに重い計算を行うと、画面操作が遅くなる可能性があります。
useMemoを使うと、依存する値が変化しない限り、以前の計算結果を再利用できます。
例えば、
-
大量データのフィルタリング
-
並び替え
-
複雑な集計
などで利用できます。
ただし、単純な計算までuseMemoにするとコードが複雑になるため、処理コストとのバランスを考えることが重要です。
useCallbackの役割
useCallbackは、関数を再利用するためのHookです。
親コンポーネントが再レンダリングされるたびに新しい関数が作成されると、子コンポーネント側でpropsが変化したと判断されることがあります。
そのような場合にuseCallbackを利用すると、関数参照を維持できます。
ただし、こちらも必要な場所だけで利用する方が分かりやすいコードになります。
状態を必要な場所に置く
アプリ全体で使わないstateを上位コンポーネントへ置くと、小さな変更でも多くのコンポーネントが再レンダリングされる可能性があります。
そのため、stateは可能な限り「実際に必要な場所の近く」に置くことが重要です。
例えば、一つのモーダルでしか使わない状態なら、ページ全体ではなくモーダルコンポーネント内で管理できます。
状態管理の範囲を小さくすることで、更新の影響範囲も減らせます。
コンポーネントを適切に分割する
一つの巨大なコンポーネントへ多くの機能をまとめると、小さなstate変更でも全体が再レンダリングされやすくなります。
例えば、
-
Header
-
Search
-
Card List
-
Pagination
-
Sidebar
などを適切に分割すると、それぞれの責任範囲が明確になります。
パフォーマンスだけでなく、コードの保守性や再利用性も向上します。
大量リストにはVirtualizationを利用する
数千件のデータを一度にDOMへ表示すると、ブラウザへの負荷が大きくなります。
このような場合はVirtualizationという方法が有効です。
Virtualizationでは、実際に画面に見えている要素だけを描画します。
ユーザーがスクロールすると、必要な部分だけを入れ替えます。
これにより、大量データでもDOM要素数を抑えることができます。
keyを正しく設定する
Reactでリストを表示するときは、各要素へkeyを設定します。
keyによって、Reactはどの要素が追加、削除、変更されたのかを判断します。
単純に配列のindexをkeyとして利用すると、並び替えや削除が発生したときに意図しない再利用が起こる場合があります。
可能であれば、
-
ID
-
一意な識別子
など、安定した値を利用することが重要です。
Code Splittingを活用する
ReactアプリのJavaScriptが大きくなると、初回表示までに時間がかかります。
Code Splittingを利用すると、機能やページごとにJavaScriptを分割できます。
例えば、
-
トップページ
-
プロフィール
-
設定画面
-
分析画面
を別々に読み込むことができます。
ユーザーが実際に開いた画面のコードだけを取得することで、初回バンドルを小さくできます。
React.lazyを利用する
ReactにはReact.lazyがあり、コンポーネントを遅延読み込みできます。
すぐに必要ではないページや機能を後から読み込むことで、初回表示を軽くできます。
Suspenseと組み合わせれば、読み込み中にローディングUIを表示することもできます。
画像も最適化する
Reactアプリだからといって、パフォーマンス問題がJavaScriptだけとは限りません。
画像サイズが大きい場合、ページ表示速度に大きな影響があります。
そのため、
-
WebP
-
AVIF
-
Lazy Loading
-
srcset
-
CDN
などを利用し、画像も最適化する必要があります。
APIリクエストを増やしすぎない
複数のコンポーネントがそれぞれ独立してAPIへアクセスすると、同じデータを何度も取得する場合があります。
そこで、
-
キャッシュ
-
データ共有
-
リクエスト統合
-
Server State管理
などを検討できます。
同じデータを繰り返し取得しないことで、ネットワーク負荷と画面更新を減らせます。
入力処理ではDebounceを使う
検索ボックスで入力するたびにAPIへアクセスすると、短時間に大量のリクエストが発生します。
例えば、ユーザーが「javascript」と入力する場合、1文字ごとに検索すると多数のAPIリクエストが送信されます。
Debounceを利用すると、入力が一定時間止まった後に検索を実行できます。
これにより、
-
API負荷の軽減
-
不要な再レンダリングの削減
-
UXの改善
JLPHにつながります。
useEffectの依存関係を確認する
useEffectの依存配列を誤ると、意図しない回数だけ処理が実行されることがあります。
例えば、API取得処理が何度も実行されると、パフォーマンスだけでなくサーバー負荷にも影響します。