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

ウェブ デベロッパーが決定すべき重要な決定の一つは、アプリのロジックとレンダリングを実装する場所です。ウェブサイトの構築方法はさまざまに あるため 簡単ではありません

この領域に関する Google の理解は、過去数年間に大規模なサイトを対象に Chrome で行った実績に基づいています。大まかに言うと、完全なリハイドレーション アプローチよりも、サーバーサイド レンダリングまたは静的レンダリングを検討することをおすすめします。

この決定を行う際に使用するアーキテクチャをより深く理解するには、各アプローチをしっかりと理解し、それらについて説明する際に使用する一貫した用語を使用する必要があります。レンダリング アプローチの違いは、ページ パフォーマンスの観点からウェブでのレンダリングのトレードオフを説明するのに役立ちます。

用語

レンダリング

サーバーサイド レンダリング(SSR)
クライアントサイド アプリまたはユニバーサル アプリをサーバー上の HTML にレンダリングする。
クライアントサイド レンダリング(CSR)
JavaScript を使用して DOM を変更し、ブラウザでアプリをレンダリングする。
水分補給
サーバーでレンダリングされた HTML の DOM ツリーとデータを再利用できるように、クライアントで JavaScript ビューを「起動」する。
事前レンダリング
ビルド時にクライアント側アプリケーションを実行して、初期状態を静的 HTML として取得します。

パフォーマンス

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

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

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

通常、サーバーサイド レンダリングでは高速の FCP が生成されます。サーバーでページロジックとレンダリングを実行することで、大量の JavaScript をクライアントに送信する必要がなくなります。これにより、ページの読み込み中にメインスレッドがブロックされる頻度が下がるため、ページの TBT が削減されます。これは INP の低下にもつながります。メインスレッドがブロックされる頻度が下がるほど、ユーザー操作が早く実行される機会が増えます。サーバーサイド レンダリングでは、テキストとリンクをユーザーのブラウザに送信するだけなので、これは理にかなっています。このアプローチは、さまざまなデバイスやネットワークの状態に適切に対応でき、ストリーミング ドキュメント解析などの興味深いブラウザ最適化を可能にします。

FCP と TTI に影響するサーバー側のレンダリングと JS の実行を示す図
サーバーサイド レンダリングを行う FCP と TTI。

サーバーサイド レンダリングを使用すると、ユーザーは CPU にバインドされた JavaScript が実行されるのを待たずにサイトを使用することができます。サードパーティの JS を使用すべきでない場合であっても、サーバーサイド レンダリングを使用して自社の JavaScript の費用を削減すれば、残りの予算を充てることができます。ただし、この方法では、サーバーでページの生成に時間がかかり、ページの TTFB が増加する可能性があります。

サーバー側レンダリングでアプリケーションにとって十分かどうかは、構築するエクスペリエンスのタイプに大きく依存します。サーバーサイド レンダリングとクライアントサイド レンダリングの適切な用途については、長年にわたって議論が行われてきましたが、ページによって、サーバーサイド レンダリングを使用するかどうかを選ぶこともできます。一部のサイトでは、ハイブリッド レンダリング手法の採用が成功しています。たとえば、Netflix のサーバーは比較的静的なランディング ページをレンダリングする一方で、インタラクションが多いページの JS をプリフェッチすることで、クライアントでレンダリングされる負荷の高いページの読み込みが速くなる可能性を高めます。

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

静的レンダリング

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

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

静的レンダリングのソリューションには、あらゆる形状とサイズがあります。Gatsby などのツールは、アプリがビルドステップとして生成されるのではなく、動的にレンダリングされているようにデベロッパーに感じてもらうように設計されています。11tyJekyllMetalsmith などの静的サイト生成ツールは、その静的性質を利用して、テンプレート主体のアプローチを提供します。

静的レンダリングの欠点の一つは、可能な 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 回(クライアントで 1 回、サーバーで 1 回)処理または再構築します。サーバーサイド レンダリングでコンテンツがすぐに表示されるからといって、必ずしも作業量が少なくなるとは限りません。サーバーによって生成された 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 の遅延読み込みを検討する必要があります。インタラクティビティがほとんどまたはまったくないエクスペリエンスでは、サーバーサイド レンダリングが、こうした問題に対するよりスケーラブルな解決策となります。

シングルページ アプリを作成する場合は、ほとんどのページで共有されるユーザー インターフェースのコア部分を識別することで、アプリケーション シェルのキャッシュ手法を適用できます。これを Service Worker と組み合わせると、ページでアプリケーション シェルの HTML と依存関係を CacheStorage から非常にすばやく読み込めるため、再アクセス時の認識されるパフォーマンスが大幅に向上します。

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

リハイドレーションは、クライアントサイドとサーバーサイドの両方のレンダリングを行うことで、レンダリングのトレードオフを解消しようとするアプローチです。ページ全体の読み込みや再読み込みなどのナビゲーション リクエストは、アプリケーションを HTML にレンダリングするサーバーによって処理され、レンダリングに使用される JavaScript とデータが、生成されるドキュメントに埋め込まれます。慎重に行うと、サーバー側のレンダリングと同様に高速な FCP を実現できます。その後、クライアント側でレンダリングを再開します。これは効果的な解決策ですが、パフォーマンス上の大きなデメリットが生じる可能性があります。

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

水分補給の問題: 2 つ分の価格で 1 つのアプリ

クライアント側の JavaScript が、サーバーが HTML をレンダリングするすべてのデータを再リクエストすることなく、サーバーにより中断された箇所を正確に「再開」するために、ほとんどのサーバーサイド レンダリング ソリューションでは、UI のデータ依存関係からのレスポンスをドキュメント内のスクリプトタグとしてシリアル化します。この方法では多くの HTML が複製されるため、リハイドレーションによって、単にインタラクティビティが遅延するだけでなく、より多くの問題が発生する可能性があります。

シリアル化された UI、インライン データ、bundle.js スクリプトを含む HTML ドキュメント
HTML ドキュメント内のコードが重複している。

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

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

TTI に悪影響を与えるクライアントのレンダリングを示す図
クライアントサイド レンダリングが TTI に及ぼす影響

ただし、リハイドレーションによるサーバー側レンダリングの希望はあります。短期的には、キャッシュに保存可能なコンテンツに対してのみサーバーサイド レンダリングを使用すると、TTFB が短縮され、事前レンダリングと同様の結果が得られます。段階的、段階的、または部分的にリハイドするのが、今後この手法をより実用的にするための鍵となる可能性があります。

サーバーサイド レンダリングのストリーミングと段階的なリハイドレート

サーバーサイド レンダリングは、ここ数年でさまざまな開発が行われました。

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

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

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

部分的な水分補給

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

部分的なハイドレーション アプローチには、それなりの問題と妥協点が伴います。これにはキャッシュ保存に関して興味深い課題がいくつかあります。また、クライアント側のナビゲーションでは、アプリケーションの不活性な部分でサーバーでレンダリングされた HTML が、ページ全体を読み込まずに利用できるとは限りません。

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

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

ブラウザと Service Worker がサーバーと通信している様子を示す三ソモーフィック レンダリングの図。
三ソモーフィック レンダリングの仕組みを示す図。

SEO に関する考慮事項

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

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

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

おわりに

レンダリングのアプローチを決定する際は、ボトルネックを測定して把握します。静的レンダリングとサーバー側レンダリングのどちらで最大限の効果が得られるかを検討します。エクスペリエンスをインタラクティブにするには、最小限の JavaScript でほとんどの HTML を提供すれば問題ありません。以下は、サーバー クライアントのスペクトルを示した便利なインフォグラフィックです。

この記事で説明するオプションの範囲を示すインフォグラフィック。
レンダリング オプションとそのトレードオフ。

クレジット

レビューやアイデアを提供してくださった皆さんに感謝します。

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