Article by: Armin Ulrich
最もパフォーマンスが高い JavaScript フレームワークはどれでしょう?React、Vue、Svelte、Angular…?この問いに答えようとするとき、私たちはしばしばリアクティビティ、バンドルサイズ、メモリ使用量などのベンチマークの比較に迷い込んでしまいます。
もちろん、パフォーマンスの高いアプリを作るために最適なフレームワークを選びたいものです。しかし、React アプリに限らず、Web アプリ全般におけるパフォーマンス最適化のベストプラクティスに従わなければ、フレームワークの性能だけではアプリの恩恵は得られません。
では、どこから始めればよいのでしょう?パフォーマンスに影響するのは何でしょうか?
このガイドでは、Reactにおけるパフォーマンス最適化の基本を解説し、この分野をさらに深く学ぶためのツールやリソースを紹介します。
なぜパフォーマンスに投資すべきか
- パフォーマンス向上 = ユーザー体験の向上
- 遅いアプリに時間を割く人はいません。人々は素早く物事を済ませたいのです。あなたのアプリはそのための道具です。パフォーマンスはアプリとブランドへの信頼を築き、良好な体験を提供する助けになります。
- 遅いアプリに時間を割く人はいません。人々は素早く物事を済ませたいのです。あなたのアプリはそのための道具です。パフォーマンスはアプリとブランドへの信頼を築き、良好な体験を提供する助けになります。
- パフォーマンス向上 = コンバージョン率と継続率の向上
- 優れたユーザー体験は、コンバージョン率と継続率(=どれだけ多くの人が登録し、使い続けるか)を高めます。つまり、パフォーマンスはアプリの成功に直接貢献します。
- 優れたユーザー体験は、コンバージョン率と継続率(=どれだけ多くの人が登録し、使い続けるか)を高めます。つまり、パフォーマンスはアプリの成功に直接貢献します。
- パフォーマンス向上 = SEOの向上
- 検索エンジンはパフォーマンスの高いページを上位に表示し、ユーザーエンゲージメントも評価対象とします。ユーザーが必要な情報を効率よく見つけられて長く滞在するなら、それがSEOパフォーマンスにも良い影響を与えます。
- 検索エンジンはパフォーマンスの高いページを上位に表示し、ユーザーエンゲージメントも評価対象とします。ユーザーが必要な情報を効率よく見つけられて長く滞在するなら、それがSEOパフォーマンスにも良い影響を与えます。
- パフォーマンス向上 = スケーラビリティの向上とコスト削減
- パフォーマンスのベストプラクティスに則ったコードベースは、システムが複雑化しても保守や拡張がしやすく、インフラコストも少なくすみます。
React によくあるパフォーマンス問題 8選とその解決方法
パフォーマンスについて話すとき、通常はアプリの読み込み時間や応答性を測る指標を指します。
読み込み時間とは、アプリが必要とするコードやアセットをすべて読み込むのにかかる時間のことです。これには、FCP(First Contentful Paint)、LCP(Largest Contentful Paint)、TTI(Time to Interactive)などの指標が用いられます。
応答性または実行時パフォーマンスは、スムーズな(再)レンダリングに関わるすべての処理を指します。ここではReactコードそのもののパフォーマンスが大きな要因になります。応答性を測るための重要な指標にはINP(Interaction to Next Paint)があります。さらに、プロファイリングやモニタリングツールを使ってトランザクションの実行時間を測定したり、フレームレート、CPUやメモリの使用量を確認したりすることも可能です。
アプリの読み込みと操作感をどちらも高速に保つには、これら両方のパフォーマンス指標を考慮する必要があります。それでは、よくある問題とReact特有の、あるいはReactに限らない解決策を見ていきましょう!
1. バンドルサイズが大きい
アプリが大きければ大きいほど、読み込みに時間がかかります。これは当然のように聞こえますが、実際にはパフォーマンスを大きく改善できるポイントのひとつです。目標は常に、可能な限り少ないコード(およびその他のアセット)をブラウザに送ることです。
- バンドラの使用と最適化
Webpack や Vite のようなバンドラは、コード分割・圧縮・ツリーシェイキング・モジュール結合・依存関係の最適化・アセットの最適化など、パフォーマンスに関わる重要な機能を標準でサポートしています。Webpack を使っている場合は、最適化が有効になるよう mode をproduction に設定するのを忘れないようにしましょう。Vite を使っている場合は、公式のパフォーマンスガイドを確認してください。 - 依存ライブラリの最適化
バンドルアナライザーやビジュアライザーなどのツールを使って、不要にバンドルを肥大化させているライブラリを見直し、削除しましょう。また、新しい外部ライブラリを追加する前には、そのサイズや影響を必ず確認する習慣をつけましょう。 - Gzip / Brotli 圧縮を有効化
Web サーバーの設定で有効にしましょう。(CDN を利用している場合は通常デフォルトで有効になっています。)
このトピックをさらに詳しく学ぶには、JavaScript バンドルサイズを最適化する8つの方法を紹介したこのブログをご覧ください。
2. アセットの最適化不足
最適化されていないアセットは、バンドルサイズに影響を与えます。画像、メディア、フォントを最適化し、効率的に配信することで、アプリの読み込み速度は大幅に向上します。
- 画像の最適化
WebP や AVIF などの最新の画像フォーマットを使用しましょう。これらは JPG や PNG よりも効率的な圧縮アルゴリズムを採用しています。ビルドプロセス中に画像のリサイズと圧縮を自動化し、<img srcset>
や<picture>
などのレスポンシブ画像技術を使って、適切な解像度の画像を読み込むようにしましょう。
- フォントの最適化
すべてのWebフォントは、読み込み時間やレンダリングに影響を与えます。本当に必要なフォントだけを読み込むようにし、可能であれば WOFF2 フォーマットを使用しましょう。自分でフォントをホストしている場合は、Munter や subfont などのツールを使って不要な文字を削除することで、フォントファイルをさらに最適化できます。 - 大きなメディアスライドショーや6種類ものフォントスタイルなどのデザイン選択が本当に必要かを見直し、パフォーマンスへの影響があることをチーム内のデザイナーに伝えるようにしましょう。
3. 不要で遅い再レンダリング
再レンダリングは処理コストが高く、遅くなりがちです。アプリ内の再レンダーの頻度・パフォーマンス・タイミングには常に注意を払う必要があります。
- Chrome DevTools のパフォーマンスタブなどのツールを使って、実行時のパフォーマンスを分析し、遅いレンダリングやボトルネックを特定する(ツールのリストは後述)。
- ネストされたコンポーネント構造によって、不要な更新が連鎖的に発生していないかを確認する
- 小さなコンポーネントの変更によって不要な再レンダリングが発生しないよう、React のメモ化テクニックを活用する
React Compiler はこうした処理をシンプルなコンポーネントに対しては自動で最適化してくれますが、より複雑なケースでは手動でのメモ化が必要になることもあります。- memo:コンポーネントをメモ化し、props が変わっていない子コンポーネントの再レンダリングを防ぐ
- useMemo:計算結果をキャッシュ
- useCallback:関数定義をキャッシュ
- 大量のリストには バーチャライゼーション(仮想化) を使い、初期レンダリングと再レンダリングの速度を改善する
- 関数はレンダリング関数の外側に定義する。あるいは、前述の useCallback フックを活用してください。
4. 状態管理とコンポーネント構造の非効率性
アプリが成長するにつれて、過剰な責務を抱えたモノリシックなコンポーネントを作ってしまっていることに気づくかもしれません。その中で、propsが複数のネスト階層を通って受け渡されていると、UI全体でコストの高い再レンダーが発生し、アプリの動作が重くなり、保守性も低下します。
- コンポーネントを適切に分割する
コンポーネントを小さく、目的が明確な単位に再構成することで、更新時にどのUIが再レンダーされるかをより細かく制御できるようになります。 - 不要なラップ要素を避けるために、Fragment を活用する
DOMに余計なノードを追加することなく、複数の子要素をグループ化できます。 - Propsのバケツリレー(prop drilling)を避けるために、Context API を使用する
ネストされたコンポーネント間でデータを直接共有できます。 - グローバルステートに過剰なデータを保持せず、必要な場所にできるだけ近いところでローカルに状態を管理する
その方が効率的で、関係のないUIまで再レンダーされるのを防げます。 - ステート管理ツールを活用する
アプリが複雑になる場合は、Redux のようなライブラリや、より軽量な代替手段である Zustand や Jōtai を使うことで、状態管理をより細かく制御できます。必要なデータだけにコンポーネントを購読させることで、効率的な更新が可能になります。
5. リクエスト過多とネットワーク遅延
データ取得とアセット配信を効率化することで、読み込み時間を短縮し、新しいデータが届いたときにもUIの応答性を保つことができます。
- 不要なAPIリクエストを回避する
データが変わっていないのに再取得しないようにし、過去に取得したデータは再利用しましょう。SWR や TanStack Query のようなライブラリを使えば、APIリクエストの管理やキャッシュを効率化できます。 - レンダリングを遅らせるブロッキングや同期リクエストを回避する
複数のリクエストはまとめて送るか、Promise を使って並列処理しましょう。ブロッキングスクリプトは async や defer 属性を使って非同期で読み込むようにしてください。 - イベントのスロットリング(throttle)とデバウンス(debounce)
関数の実行頻度を制御し、過剰な更新を防ぐために使います。スクロールやウィンドウのリサイズ、マウスの動きに対する更新はスロットリングで頻度を制限し、検索の実行などはユーザーの入力が止まってから(例:最後のキー入力から300ms後)に実行するようデバウンスを使います。これには useHooks ライブラリの useDebounce や useThrottle フックを使用できます。 - ユーザートラッキングの過剰な利用を避ける
ユーザーデータの収集はアプリ改善の判断材料になりますが、トラッキングスクリプトや過度なアナリティクスはアプリを著しく遅くします。クライアント側が重いトラッキングソリューションは肥大化し、繰り返しリクエストを送信します。分析サーバーへのイベント送信はバッチ処理でまとめ、リクエストを最小限に抑えましょう。 - サーバーサイドレンダリング(SSR)または静的サイト生成(SSG)を利用する
ビルド時やリクエスト時にサーバーでデータを取得することで、必要な情報を含んだ状態でコンテンツをレンダリングできます。 - 地理的に近いサーバーからコンテンツを配信するCDNを活用する
より効率的にコンテンツを届けましょう。
6. 不要なリソース使用
過剰または非効率な処理によって、リソースを無駄に消費し、描画の遅延やメモリ使用量の増加、UIの遅延・無反応を引き起こすことがあります。
- CPU負荷の高い処理には Web Worker を使用する
リクエスト結果のソートやフィルタリング、画像処理などの重い処理はメインスレッドから切り離し、Web Worker を使用し、実行することで、UIのブロックを回避できます。 - メモリリークを防ぐ
古いイベントリスナーやタイマーなど、解放されていないリソースはメモリリークの原因となり、時間とともにリソース使用量を増加させます。useEffect フックでは、コンポーネントのアンマウントや更新時に不要なリソースを確実に解放するため、クリーンアップ関数を返すようにしましょう。 - JSアニメーションではなくCSSアニメーションを使用する
JSで要素をアニメーションさせるとリソース消費が激しく、メインスレッドをブロックすることがあります。一方、CSSアニメーションはGPU上で実行され、描画やユーザー操作に干渉しません。スムーズな遷移、拡大縮小、回転、要素の移動などにはCSSの方が適しています。
7. 非効率な読み込み戦略
リソースには重要度の差があります。初期レンダリングをすばやく完了するために必要なものは、理想的には最優先で読み込まれるべきです。しかし実際には、不要なスクリプトがメインスレッドを無意識にブロックしてしまい、全体を遅くしてしまうことがあります。リソースやアセットの読み込みを賢く設計することで、アプリの体感パフォーマンスは大きく向上します。
- すべてのJSやCSSを最初に一括読み込みしない
初期描画の遅延を防ぐため、コード分割(Webpack や Vite ではルート単位やコンポーネント単位で分割可能)を活用しましょう。また、重要なCSSは HTMLの <head> にインラインで書くことも検討してください。 - コンポーネントを必要なときに読み込む
モーダルやドロップダウンなどのコンポーネントはReact.lazy
を使って遅延読み込みし、待機中はSuspense
でプレースホルダーやフォールバックUIを表示しましょう。 - まだ必要ないものは読み込まない
大量のデータリストでは、表示されている部分だけを描画するバーチャライゼーションを活用しましょう。また、IntersectionObserver
API を使えば、セクションや動画・グラフなどの重いコンテンツを、画面に入るタイミングまで遅延させて読み込むことができます。 - 重要なリソースは優先的に、そうでないものは後回しに
フォントなどのアセットはpreload
指示子で優先的に読み込み、将来的に使う可能性が高いリソースはアイドル時間にprefetch
で先読みさせましょう。また、アセットをキャッシュして、同じものを何度もダウンロードさせないようにしましょう。
「From LCP to CLS: Improve your Core Web Vitals with Image Loading Best Practices」というブログで、レイジーローディング(遅延読み込み)について詳しく学ぶことができます。
8. 体感パフォーマンスの低下
実際に速く読み込まれていても、ユーザーが速いと感じるとは限りません。レンダリングの遅れ、操作への即時反応の欠如、レイアウトのズレなどにより、ユーザーはアプリを「遅い」と感じます。
- Concurrent Rendering を(慎重に)使う
useTransition
フックを使えば、重要度の低いレンダリング処理を中断し、重要な更新を優先することで UI の応答性を高められます。たとえば、ユーザーが文字入力をしている最中に重たい検索処理を並行して行うと遅く感じられますが、入力操作を優先すればスムーズな体験になります。ただし、Concurrent Rendering を乱用すると予期しない再レンダーが起こることもあるので、開発者ツールやプロファイラーで影響を確認しながら使うようにしましょう。 - レイアウトのズレを最小限に抑える
レイアウトシフトとは、画像や広告などの要素が初期コンテンツの読み込み後に表示され、レイアウトが突然ずれてしまう現象です。これは Core Web Vitals のひとつである CLS(Cumulative Layout Shift)という指標で評価されます。
最悪の場合、ユーザーがすでに読んでいたテキストが急に動いてしまいます。(レシピブログを最近見たことがあれば、どれだけイライラするか分かりますよね!)
このようなことを防ぐには、画像・動画・動的コンテンツに対して、あらかじめ固定サイズでスペースを確保しておきましょう。 - コンテンツのプレビューを表示する
フィードなどのデータを読み込むのに時間がかかる場合は、スケルトンローダーやプレースホルダーを使って、読み込み中であることを視覚的に伝えましょう。これは SNS でもよく使われており、読み込みに対するストレスを大きく軽減できるとされています。 - 読み込み中であることを明確に伝える
フォーム送信や API 呼び出しなどで処理に時間がかかると、何の表示もない場合、ユーザーはアプリがフリーズしたと思ってしまいます。そうならないように、ローディングスピナーやステータスメッセージなどを表示し、「待っている状態」であることを明示しましょう。 - アニメーションのやりすぎに注意
トランジションやアニメーションは使いすぎると逆効果です。特に繰り返し発生する操作では、最初は良くてもすぐに煩わしく感じられるようになります。また、インタラクションに対するアニメーションの長さは 300ms 以下にとどめるのが推奨されます。
React パフォーマンスを測定・改善するためのツール
アプリのパフォーマンスを測定する
「測定できないものは最適化できない。」React アプリの現在のパフォーマンス状態を監査するためのツールは多数あります。
- Chrome パフォーマンスパネル
Chrome DevTools のパフォーマンスタブにアクセスし、インタラクションを記録することで、スクリプト処理、レンダリング、ペイントの詳細なタイムラインを取得できます。これにより、レンダリングの遅延やレイアウトシフトによるページ読み込みの遅さの原因を特定できます。 - Lighthouse / Lighthouse CI
パフォーマンス、アクセシビリティ、SEO などを Lighthouse のスコアで自動的に監査できます。FCP(最初のコンテンツ描画)や TTI(インタラクティブまでの時間)などの指標に対するアプリのスコアが表示されます。Chrome DevTools から監査を開始することも、CI パイプラインに統合することもできます。 - React DevTools Profiler
React DevTools の Profiler タブを使って、各コンポーネントのレンダリングにかかる時間を可視化し、パフォーマンスの遅いコンポーネントを特定・最適化できます。 - WebPageTest
低速ネットワークや異なる地域でのサイトの動作をWebPageTestでテストすることができます。 - Google PageSpeed Insights & Analytics
PageSpeed Insights を使って、Core Web Vitals に含まれる指標の測定結果を取得できます。また、異なるデバイスでのパフォーマンスも確認可能です。Google Analytics を使えば、ユーザーの実際の使用状況データやカスタムイベントのパフォーマンスも追跡できます。 - ロードテスト
k6 や locust.io(どちらも OSS)、Artillery などのツールを使って、高トラフィック時のパフォーマンスのボトルネックを特定できます。
パフォーマンスのデバッグ
開発中に問題を調査するには、以下のツールが役立ちます。
- Profiler
特定のコンポーネントを <Profiler> でラップすることで、そのレンダリングパフォーマンスを測定できます。 - Why did you render?
React / React Native アプリで不要な再レンダリングの可能性を通知し、特定のコンポーネントのレンダリングを追跡できます。 - React Scan
パフォーマンス上の問題を自動的に検出し、最適化すべきコンポーネントをハイライトします。コード変更なしで簡単に導入できます。 - Million
Million Lint は、パフォーマンスに影響する React コードを自動検出し、改善方法を提案する IDE 拡張機能です。 - バンドルアナライザー
Webpack や Vite のバンドル分析ツールを使えば、大きなサードパーティ製ライブラリを特定し、バンドルサイズへの影響を確認できます。
パフォーマンスのモニタリング
時間の経過とともに、実環境での React アプリのパフォーマンスを継続的に把握したい場合は、モニタリングサービスの導入が有効です。
フルスタックのアプリケーション監視ツールには、通常パフォーマンスモニタリング機能が含まれています。これを導入すれば、たとえば一時的な API 遅延など、ユーザーに影響がある問題を即座に検知できます。
React の統合機能を提供し、コンポーネントのパフォーマンスに関する詳細な情報を提供するサービスは数多く存在します。宣伝になりますが、Sentry React のパフォーマンス監視にぜひご検討ください。
Sentry はエラーやパフォーマンスの問題をリアルタイムで報告し、修正に役立つコードレベルの状況情報も収集します。
パフォーマンスに関する追加リソース
本ガイドはあくまで出発点です。React アプリ最適化のヒントや基礎的な知識を得られたことを願います。
さらに学びたい方のために、追加のリソースを以下に紹介します。
Original Page: React.js Performance Guide
IchizokuはSentryと提携し、日本でSentry製品の導入支援、テクニカルサポート、ベストプラクティスの共有を行なっています。Ichizokuが提供するSentryの日本語サイトについてはこちらをご覧ください。またご導入についての相談はこちらのフォームからお気軽にお問い合わせください。