ウェブ上でのレンダリング

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

この分野に関する 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 を送信することを回避できます。これにより、ページの TTBT が短縮され、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 などのツールは、ビルドステップとして生成されるのではなく、アプリケーションが動的にレンダリングされているようにデベロッパーが感じられるように設計されています。11tyJekyllMetalsmith などの静的サイト生成ツールは、静的という性質を活かし、テンプレート駆動型のアプローチを提供しています。

静的レンダリングの欠点の 1 つは、考えられるすべての 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 を構築する際に興味深い判断を必要とすることもあります。フルページの Service Worker キャッシュを使用する方がよいか、個々のコンテンツをサーバーでレンダリングする方がよいか。

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

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

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

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

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

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

シングルページ アプリケーションを構築している場合、ほとんどのページで共有されるユーザー インターフェースのコア部分を特定することで、アプリケーション シェル キャッシュ保存の手法を適用できます。サービス ワーカーと組み合わせることで、再訪問時の体感パフォーマンスを大幅に改善できます。これは、ページがアプリケーション シェルの 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 がページ全体の読み込みなしで利用できるとは限りません。

Trisomorphic レンダリング

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

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

SEO に関する考慮事項

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

迷った場合は、モバイル フレンドリー テストツールを使用して、選択したアプローチが期待どおりに機能するかどうかをテストすることをおすすめします。このツールでは、Google のクローラにページがどのように表示されるかのプレビュー、JavaScript の実行後に検出されたシリアル化された HTML コンテンツ、レンダリング中に発生したエラーが表示されます。

モバイル フレンドリー テストの UI。

まとめ

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

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

クレジット {:#credits}

レビューとインスピレーションをいただいた皆様に感謝いたします。

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