ウェブでのレンダリング

公開日: 2019 年 2 月 6 日、最終更新日: 2026 年 1 月 5 日

ウェブ デベロッパーが下さなければならない重要な決定の一つは、アプリケーションのどこにロジックとレンダリングを実装するかということです。ウェブサイトの構築方法は多岐にわたるため、これは難しい場合があります。

この分野に関する Google の理解は、過去数年間にわたって Chrome で大規模なサイトと対話してきた経験に基づいています。大まかに言うと、完全なリハイドレーション アプローチではなく、サーバーサイド レンダリングまたは静的レンダリングを検討することをおすすめします。

この決定を行う際に選択するアーキテクチャをより深く理解するには、一貫した用語と、各アプローチの共有フレームワークが必要です。これにより、ページ パフォーマンスの観点から、各レンダリング アプローチのトレードオフをより適切に評価できます。

用語

まず、使用する用語をいくつか定義します。

レンダリング

サーバーサイド レンダリング(SSR)
サーバー上でアプリをレンダリングして、JavaScript ではなく HTML をクライアントに送信する。
クライアントサイド レンダリング(CSR)
ブラウザでアプリをレンダリングし、JavaScript を使用して DOM を変更する。
事前レンダリング
ビルド時にクライアントサイド アプリケーションを実行して、その初期状態を静的 HTML としてキャプチャする。この意味での「事前レンダリング」は、今後のナビゲーションのブラウザ事前レンダリングとは異なります。
水分補給
クライアントサイド スクリプトを実行して、サーバーでレンダリングされた HTML にアプリケーションの状態とインタラクティビティを追加する。ハイドレーションは、DOM が変更されないことを前提としています。
リハイドレーション
ハイドレーションと同じ意味で使用されることが多いですが、リハイドレーションは、最初のハイドレーション後も含めて、DOM を最新の状態に定期的に更新することを意味します。

パフォーマンス

Time to First Byte(TTFB)
リンクをクリックしてから、新しいページでコンテンツの最初のバイトが読み込まれるまでの時間。
First Contentful Paint(FCP)
リクエストされたコンテンツ(記事本文など)が表示されるまでの時間。
Interaction to Next Paint(INP)
ページがユーザー入力に一貫して迅速に応答するかどうかを評価する代表的な指標。
Total Blocking Time(TBT)
INP のプロキシ指標。ページの読み込み中にメインスレッドがブロックされた時間を計算します。

サーバーサイド レンダリング

サーバーサイド レンダリングでは、ナビゲーションに応じてサーバー上のページの完全な HTML が生成されます。これにより、ブラウザがレスポンスを受け取る前にレンダラが処理するため、クライアントでのデータ取得とテンプレート化のための追加の往復を回避できます。

サーバーサイド レンダリングでは、一般的に FCP が高速になります。サーバーでページロジックを実行してレンダリングすると、クライアントに大量の JavaScript を送信する必要がなくなります。これにより、ページの TBT が短縮され、ページの読み込み中にメインスレッドがブロックされる頻度が減るため、INP の短縮にもつながります。メインスレッドがブロックされる頻度が低くなると、ユーザー インタラクションがより早く実行される機会が増えます。

これは理にかなっています。サーバーサイド レンダリングでは、テキストとリンクをユーザーのブラウザに送信するだけだからです。このアプローチは、さまざまなデバイスやネットワーク条件で適切に機能し、ストリーミング ドキュメントの解析など、興味深いブラウザの最適化を可能にします。

サーバーサイド レンダリングと JavaScript の実行が FCP と TTI に影響することを示す図。
サーバーサイド レンダリングでの FCP と TTI。

サーバーサイド レンダリングを使用すると、ユーザーがサイトを使用できるようになる前に、CPU バウンドの JavaScript の実行を待つ必要がなくなります。サードパーティの JavaScript を回避できない場合でも、サーバーサイド レンダリングを使用して独自のファーストパーティの JavaScript コストを削減することで、残りの予算を増やすことができます。ただし、このアプローチにはトレードオフが 1 つあります。サーバーでページを生成するのに時間がかかるため、ページの TTFB が増加する可能性があります。

サーバーサイド レンダリングがアプリケーションに十分かどうかは、構築するエクスペリエンスの種類によって大きく異なります。サーバーサイド レンダリングとクライアントサイド レンダリングのどちらを適用するのが正しいかについては、長年にわたって議論が続いていますが、一部のページにサーバーサイド レンダリングを使用し、他のページには使用しないという選択も可能です。一部のサイトでは、ハイブリッド レンダリング手法が採用されています。 たとえば、Netflix は、比較的静的なランディング ページをサーバーでレンダリングし、インタラクションの多いページの JavaScript をprefetchingすることで、クライアントでレンダリングされる重いページを高速に読み込むことができます。

最新のフレームワーク、ライブラリ、アーキテクチャの多くでは、クライアントとサーバーの両方で同じアプリケーションをレンダリングできます。これらの手法は、サーバーサイド レンダリングに使用できます。ただし、サーバーとクライアントの両方でレンダリングが行われるアーキテクチャは、パフォーマンス特性とトレードオフが大きく異なる独自のソリューション クラスです。React ユーザーは、サーバー DOM API またはそれに基づいて構築されたソリューション(Next.js など)をサーバーサイド レンダリングに使用できます。Vue ユーザーは、Vue のサーバーサイド レンダリング ガイドまたは Nuxt を使用できます。Angular には Universal があります。

ただし、最も一般的なソリューションでは、何らかのハイドレーションが使用されているため、ツールで使用されているアプローチに注意してください。

静的レンダリング

静的レンダリングはビルド時に行われます。このアプローチでは、ページ上のクライアントサイド JavaScript の量を制限する限り、FCP が高速になり、TBT と INP も低くなります。また、サーバーサイド レンダリングとは異なり、ページの HTML をサーバー上で動的に生成する必要がないため、一貫して高速な TTFB を実現できます。一般に、静的レンダリングとは、URL ごとに個別の HTML ファイルを事前に生成することを意味します。HTML レスポンスを事前に生成することで、静的レンダリングを複数の CDN にデプロイして、エッジ キャッシングを活用できます。

静的レンダリングとオプションの JavaScript 実行が FCP と TTI に影響することを示す図。
静的レンダリングでの FCP と TTI。

静的レンダリングのソリューションには、さまざまな形やサイズがあります。Gatsby などのツールは、デベロッパーがアプリケーションをビルドステップとして生成するのではなく、動的にレンダリングしているように感じられるように設計されています。11ty、Jekyll、Metalsmith などの静的サイト生成ツールは、静的な性質を活かして、よりテンプレート駆動型のアプローチを提供します。

静的レンダリングのデメリットの一つは、考えられるすべての URL に対して個別の HTML ファイルを生成する必要があることです。これは、URL を事前に予測する必要がある場合や、一意のページが多数あるサイトでは、困難または不可能な場合があります。

React ユーザーは、Gatsby、Next.js 静的エクスポート、Navi に精通しているかもしれません。これらはすべて、コンポーネントからページを作成するのに便利です。ただし、静的レンダリングと事前レンダリングの動作は異なります。静的にレンダリングされたページは、クライアントサイドの JavaScript をあまり実行しなくてもインタラクティブになります。一方、事前レンダリングは、ページを真にインタラクティブにするためにクライアントで起動する必要があるシングルページ アプリケーションの FCP を改善します。

特定のソリューションが静的レンダリングか事前レンダリングかわからない場合は、JavaScript を無効にして、テストするページを読み込んでみてください。静的にレンダリングされたページの場合、ほとんどのインタラクティブ機能は JavaScript がなくても存在します。事前レンダリングされたページには、JavaScript が無効になっているリンクなど、いくつかの基本的な機能が残っている場合がありますが、ページのほとんどは不活性です。

もう 1 つの便利なテストは、Chrome DevTools のネットワーク スロットリングを使用して、ページがインタラクティブになる前にダウンロードされる JavaScript の量を確認することです。一般に、プリレンダリングをインタラクティブにするには、より多くの JavaScript が必要になります。また、その JavaScript は、静的レンダリングで使用されるプログレッシブ エンハンスメント アプローチよりも複雑になる傾向があります。

サーバーサイド レンダリングと静的レンダリング

サーバーサイド レンダリングは、動的な性質によりコンピューティング オーバーヘッドのコストが大きくなる可能性があるため、すべてのソリューションに最適とは限りません。多くのサーバーサイド レンダリング ソリューションは、早期にフラッシュせず、TTFB を遅らせたり、送信されるデータを 2 倍にしたりします(たとえば、クライアントの JavaScript で使用されるインライン状態)。React では、renderToString() は同期的でシングルスレッドであるため、処理が遅くなる可能性があります。新しい React サーバー DOM API はストリーミングをサポートしています。これにより、HTML レスポンスの最初の部分をブラウザに早く送信し、残りの部分はサーバーで生成できます。

サーバーサイド レンダリングを「適切」に行うには、コンポーネント キャッシュのソリューションを見つけるか構築する、メモリ消費を管理する、メモ化手法を使用する、などの懸念事項があります。多くの場合、同じアプリをクライアントとサーバーで 2 回処理または再ビルドすることになります。サーバーサイド レンダリングでコンテンツをより早く表示しても、必ずしも作業量が減るわけではありません。サーバーで生成された HTML レスポンスがクライアントに到着した後に、クライアントで多くの作業を行う必要がある場合、ウェブサイトの TBT と INP が高くなる可能性があります。

サーバーサイド レンダリングでは、URL ごとにオンデマンドで HTML が生成されますが、静的にレンダリングされたコンテンツを配信するよりも時間がかかることがあります。追加の作業を行うことができる場合は、サーバーサイド レンダリングと HTML キャッシュを使用することで、サーバーのレンダリング時間を大幅に短縮できます。サーバーサイド レンダリングの利点は、静的レンダリングよりも多くの「ライブ」データを取得し、より完全なリクエスト セットに応答できることです。パーソナライズが必要なページは、静的レンダリングではうまく機能しないリクエストの種類の具体例です。

サーバーサイド レンダリングは、PWA を構築する際にも興味深い決定事項を提示します。フルページのサービス ワーカー キャッシュを使用するのと、個々のコンテンツをサーバーでレンダリングするのとでは、どちらがよいでしょうか。

クライアントサイド レンダリング

クライアントサイド レンダリングとは、JavaScript を使用してブラウザで直接ページをレンダリングすることです。すべてのロジック、データの取得、テンプレート化、ルーティングは、サーバーではなくクライアントで処理されます。その結果、サーバーからユーザーのデバイスに渡されるデータが増え、それには独自のトレードオフが伴います。

クライアントサイド レンダリングは、モバイル デバイス向けに高速化して維持することが難しい場合があります。JavaScript の予算を厳しく管理し、できるだけ少ないラウンドトリップで価値を提供することで、クライアントサイド レンダリングで純粋なサーバーサイド レンダリングのパフォーマンスをほぼ再現できます。<link rel=preload> を使用して重要なスクリプトとデータを配信することで、パーサーの動作を高速化できます。また、PRPL などのパターンを使用して、最初のナビゲーションと後続のナビゲーションが瞬時に行われるようにすることをおすすめします。

クライアントサイド レンダリングが FCP と TTI に影響することを示す図。
クライアントサイド レンダリングでの FCP と TTI。

クライアントサイド レンダリングの主なデメリットは、アプリケーションの成長に伴って必要な JavaScript の量が増加する傾向があり、ページの INP に影響する可能性があることです。これは、新しい JavaScript ライブラリ、ポリフィル、サードパーティ コードが追加されると特に難しくなります。これらは処理能力を競い合い、多くの場合、ページのコンテンツをレンダリングする前に処理する必要があります。

クライアントサイド レンダリングを使用し、大規模な JavaScript バンドルに依存するエクスペリエンスでは、ページ読み込み時の TBT と INP を低減するために積極的なコード分割を検討する必要があります。また、JavaScript の遅延読み込みにより、ユーザーが必要とするものだけを必要なときに提供することも検討してください。インタラクティビティがほとんどない、またはまったくないエクスペリエンスの場合、サーバーサイド レンダリングは、これらの問題に対するよりスケーラブルなソリューションとなります。

シングルページ アプリケーションを構築する場合は、ほとんどのページで共有されるユーザー インターフェースのコア部分を特定することで、アプリケーション シェルのキャッシュ保存手法を適用できます。これを Service Worker と組み合わせると、ページがアプリケーション シェルの HTML と依存関係を CacheStorage から非常に迅速に読み込めるようになるため、再訪問時の体感パフォーマンスを大幅に向上させることができます。

リハイドレーションはサーバーサイド レンダリングとクライアントサイド レンダリングを組み合わせたもの

ハイドレーションは、クライアントサイド レンダリングとサーバーサイド レンダリングの両方を行うことで、両者のトレードオフを軽減するアプローチです。ナビゲーション リクエスト(フルページの読み込みや再読み込みなど)は、アプリケーションを HTML にレンダリングするサーバーによって処理されます。次に、レンダリングに使用される JavaScript とデータが、結果のドキュメントに埋め込まれます。慎重に行うと、サーバーサイド レンダリングのように高速な FCP を実現し、クライアントで再度レンダリングすることで「ピックアップ」します。

これは効果的なソリューションですが、パフォーマンスに大きな影響を与える可能性があります。

リハイドレーションを伴うサーバーサイド レンダリングの主なデメリットは、FCP が改善されても、TBT と INP に大きな悪影響を及ぼす可能性があることです。サーバーサイドでレンダリングされたページは、読み込まれてインタラクティブであるように見えますが、コンポーネントのクライアントサイド スクリプトが実行され、イベント ハンドラがアタッチされるまで、実際には入力に応答できません。モバイルでは、これに数分かかることがあり、ユーザーを混乱させ、不満を抱かせる可能性があります。

リハイドレーションの問題: 2 つのアプリの価格で 1 つのアプリ

クライアントサイドの JavaScript が、サーバーが HTML をレンダリングしたすべてのデータを再リクエストすることなく、サーバーが中断したところから正確に引き継ぐために、ほとんどのサーバーサイド レンダリング ソリューションは、UI のデータ依存関係からのレスポンスをドキュメント内のスクリプトタグとしてシリアル化します。これにより大量の HTML が重複するため、リハイドレーションはインタラクティビティの遅延だけでなく、より多くの問題を引き起こす可能性があります。

シリアル化された UI、インライン データ、bundle.js スクリプトを含む HTML ドキュメント。

サーバーは、ナビゲーション リクエストに応答してアプリケーションの UI の説明を返しますが、その UI の作成に使用されたソースデータと、UI の実装の完全なコピーも返します。このコピーはクライアントで起動されます。bundle.js の読み込みと実行が完了するまで、UI はインタラクティブになりません。

サーバーサイド レンダリングとリハイドレーションを使用する実際のウェブサイトから収集されたパフォーマンス指標は、これが最適なオプションになることはめったにないことを示しています。最も重要な理由は、ユーザー エクスペリエンスへの影響です。ページが準備完了しているように見えても、インタラクティブな機能が動作しないことがあります。

クライアントサイド レンダリングが TTI に及ぼす悪影響。

リハイドレーションによるサーバーサイド レンダリングには期待が持てます。短期的には、キャッシュに保存可能なコンテンツにのみサーバーサイド レンダリングを使用することで、TTFB を短縮し、事前レンダリングと同様の結果を得ることができます。段階的、漸進的、または部分的にリハイドレートすることが、将来的にこの手法をより実用的なものにする鍵となるかもしれません。

サーバーサイド レンダリングをストリーミングして段階的にリハイドレートする

サーバーサイド レンダリングは、ここ数年で数多くの進化を遂げてきました。

ストリーミング サーバーサイド レンダリングを使用すると、HTML をチャンクで送信できます。ブラウザは、受信したチャンクを段階的にレンダリングできます。これにより、マークアップをユーザーに迅速に提供し、FCP を高速化できます。React では、同期 renderToString() と比較して、renderToPipeableStream() でストリームが非同期であるため、バックプレッシャーが適切に処理されます。

プログレッシブ リハイドレーションも検討に値します(React に実装されています)。このアプローチでは、サーバーでレンダリングされたアプリケーションの個々の部分が時間の経過とともに「起動」されます。これは、アプリケーション全体を一度に初期化する現在の一般的なアプローチとは異なります。これにより、ページの優先度の低い部分のクライアントサイドのアップグレードを延期して、メインスレッドをブロックしないようにできるため、ページをインタラクティブにするために必要な JavaScript の量を減らすことができます。これにより、ユーザーが操作を開始してから、ユーザー インタラクションがより早く発生するようになります。

プログレッシブ リハイドレーションは、サーバーサイド レンダリングのリハイドレーションで最もよくある落とし穴の 1 つを回避するのにも役立ちます。サーバーでレンダリングされた DOM ツリーが破棄され、すぐに再構築されるというものです。これは、最初の同期クライアントサイド レンダリングで、まだ準備ができていないデータ(多くの場合、まだ解決されていない Promise)が必要だったことが原因です。

部分的なリハイドレーション

部分的なリハイドレーションは実装が難しいことがわかっています。このアプローチは、プログレッシブ リハイドレーションの拡張機能であり、ページの個々の部分(コンポーネント、ビュー、ツリー)を分析し、インタラクティビティがほとんどない部分やリアクティビティがない部分を特定します。これらのほとんどが静的な部分ごとに、対応する JavaScript コードが不活性な参照と装飾機能に変換され、クライアントサイドのフットプリントがほぼゼロに削減されます。

部分的なリハイドレーションのアプローチには、独自の問題と妥協が伴います。キャッシュ保存に関する興味深い課題が生じます。また、クライアントサイド ナビゲーションでは、アプリケーションの不活性部分のサーバー レンダリング HTML が、ページ読み込みなしで利用できるとは限りません。

トリソモーフィック レンダリング

サービス ワーカーを使用できる場合は、トライソモーフィック レンダリングを検討してください。この手法を使用すると、最初のナビゲーションまたは JavaScript 以外のナビゲーションにストリーミング サーバーサイド レンダリングを使用し、インストール後に Service Worker がナビゲーションの HTML のレンダリングを引き継ぐことができます。これにより、キャッシュに保存されたコンポーネントとテンプレートを最新の状態に保ち、同じセッションで新しいビューをレンダリングするための SPA スタイルのナビゲーションを有効にできます。この方法は、サーバー、クライアント ページ、Service Worker の間で同じテンプレートとルーティング コードを共有できる場合に最適です。

トリソモルフィック レンダリング。ブラウザとサービス ワーカーがサーバーと通信していることを示しています。

SEO に関する考慮事項

ウェブ レンダリング戦略を選択する際、チームは SEO の影響を考慮することがよくあります。サーバーサイド レンダリングは、クローラーが解釈できる「完成された外観」のエクスペリエンスを提供するための一般的な選択肢です。クローラーは JavaScript を理解できますが、レンダリング方法には制限があることがよくあります。クライアントサイド レンダリングは機能しますが、多くの場合、追加のテストとオーバーヘッドが必要になります。最近では、アーキテクチャがクライアントサイドの JavaScript に大きく依存している場合、ダイナミック レンダリングも検討に値するオプションとなっています。

まとめ

レンダリングのアプローチを決定する際は、ボトルネックを測定して把握します。静的レンダリングまたはサーバーサイド レンダリングで、ほとんどの処理を行えるかどうかを検討します。インタラクティブなエクスペリエンスを実現するために、最小限の JavaScript で HTML を送信しても問題ありません。サーバーとクライアントのスペクトルを示す便利なインフォグラフィックを次に示します。

レンダリング オプションとそのトレードオフ。

クレジット

レビューとインスピレーションを提供してくれたすべての人に感謝します。

Jeffrey Posnick、Houssein Djirdeh、Shubhie Panicker、Chris Harrelson、Sebastian Markbåge。