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

アプリケーションのどこにロジックとレンダリングを実装すればよいでしょうか。サーバーサイド レンダリングを使用すべきか水分補給はどうですか?答えを探してみましょう。

デベロッパーはしばしば、アプリケーションのアーキテクチャ全体に影響を与える決定に直面します。ウェブ デベロッパーが決定すべき重要な決定の一つは、アプリケーションのロジックとレンダリングを実装する場所です。ウェブサイトの構築にはさまざまな方法があるため、これは難しい場合があります。

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

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

用語

レンダリング

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

パフォーマンス

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

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

通常、サーバーサイド レンダリングでは高速の FCP が生成されます。サーバーでページロジックとレンダリングを実行することで、大量の JavaScript をクライアントに送信することを回避できます。これにより、ページ読み込み時にメインスレッドがブロックされる頻度が下がるため、ページの TBT が削減されます。これは INP の低下にもつながります。メインスレッドがブロックされる頻度が下がると、ユーザー操作が早く実行される機会が多くなります。サーバーサイド レンダリングでは、テキストとリンクをユーザーのブラウザに送信するだけなので、これは理にかなっています。この方法は、さまざまなデバイスやネットワーク条件に適切に対応でき、ストリーミング ドキュメントの解析など、ブラウザの最適化に活用できます。

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

サーバーサイド レンダリングを使用すると、ユーザーは CPU バウンドの JavaScript が実行されるのを待たずにサイトを利用することができます。サードパーティの JavaScript を使用すべきでない場合であっても、サーバーサイド レンダリングを使用して自社の JavaScript の費用を削減すれば、残りの部分にかかる予算を増やすことができます。ただし、この方法にはトレードオフが 1 つあります。サーバーでのページの生成に時間がかかり、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 実行を示す図。

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

静的レンダリングの欠点の一つは、可能性のある URL ごとに個別の HTML ファイルを生成する必要があることです。これは、URL を事前に予測できない場合や、一意のページが多数あるサイトにとっては難しい、あるいは現実的でないことさえあります。

React のユーザーは、Gatsby、Next.js 静的エクスポートNavi に馴染みがあるかもしれません。これらはすべて、コンポーネントを使用してページを作成するのに役立ちます。ただし、静的レンダリングと事前レンダリングの違いを理解しておくことが重要です。静的レンダリングは、クライアント側の JavaScript をあまり実行する必要がないインタラクティブです。一方、事前レンダリングは、ページを真にインタラクティブにするためにクライアントで起動しなければならない単一ページ アプリケーションの FCP を改善します。

特定のソリューションが静的レンダリングと事前レンダリングのどちらであるか不明な場合は、JavaScript を無効にしてから、テストするページを読み込みます。静的にレンダリングされるページでは、ほとんどの機能は JavaScript を有効にしていなくても使用できます。事前レンダリングされたページでも、リンクなどの基本的な機能は残る場合がありますが、ほとんどのページは不活性です。

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

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

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

サーバーサイド レンダリングを「適切」に行うには、コンポーネント キャッシュのソリューションを見つけるか、ソリューションを構築すること、メモリ消費の管理、メモリ化手法の適用など、さまざまな懸念事項が必要になります。一般的に、同じアプリケーションを複数回(クライアント上で 1 回、サーバーで 1 回)処理/再構築することにします。サーバーサイド レンダリングによって、何かが早く表示されるからといって、急に作業量が少なくなるとは限りません。サーバーによって生成された HTML レスポンスがクライアントに到着した後、クライアント側で多くの作業を行っていると、ウェブサイトの TBT と INP が高くなる可能性があります。

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

PWA を作成する際は、サーバーサイド レンダリングも重要な判断材料となります。全画面の Service Worker キャッシュを使用するのと、個々のコンテンツをサーバーでレンダリングするのとではどちらがよいですか?

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

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

モバイル デバイスでは、クライアントサイド レンダリングを高速で維持することが難しい場合があります。最小限の作業で、クライアントサイド レンダリングは純粋なサーバーサイド レンダリングのパフォーマンスに近づくことができ、限られた JavaScript 予算を維持し、可能な限り少ないラウンド トリップで価値を提供できます。<link rel=preload> を使用すると、重要なスクリプトやデータをより早く配信できるため、パーサーをより早く利用できるようになります。PRPL のようなパターンも、初期ナビゲーションと後続のナビゲーションが瞬時に感じられるようにするには、評価する価値があります。

FCP と TTI に影響するクライアント側のレンダリングを示す図。

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

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

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

リハイドレーションによるサーバーサイド レンダリングとクライアントサイド レンダリングの併用

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

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

ページが読み込まれたようでしばらくの間、クリックやタップしても何も起こらないという経験はおそらくあります。ページを操作しようとしたときに、なぜ何も起こらないのか疑問に思われるため、ユーザーはすぐにイライラします。

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

リハイドレーションの問題は、JavaScript によるインタラクティビティの遅延よりも問題になることがよくあります。サーバーが HTML のレンダリングに使用したすべてのデータを再リクエストすることなく、クライアント側の JavaScript がサーバーで中断した箇所を正確に「再開」できるようにするために、現在のサーバー側のレンダリング ソリューションは通常、UI のデータ依存関係からのレスポンスをスクリプトタグとしてドキュメントにシリアル化します。生成された HTML ドキュメントには、大量の重複が含まれています。

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

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

サーバーサイド レンダリングとリハイドレーションを使用して実際のウェブサイトから収集されたパフォーマンス指標から、この API の使用は推奨されないことがわかります。結局のところ、その理由はユーザー エクスペリエンスにあります。ユーザーはすぐに「不気味な谷」に陥り、ページの準備が整っているように見えてもインタラクティビティが失われているように感じられます。

TTI に悪影響を及ぼすクライアントのレンダリングを示す図。

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

ストリーミング サーバー側レンダリングとプログレッシブ リハイドレーション

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

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

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

プログレッシブ リハイドレーションでは、サーバーサイド レンダリングのリハイドレーションでよくある問題の一つを回避できます。この落とし穴では、サーバーでレンダリングされた DOM ツリーが破棄されてからすぐに再構築されます。多くの場合、最初の同期クライアントサイド レンダリングでは、準備が整っていないデータが必要であり、Promise の解決を待っていることが考えられます。

部分的な水分補給

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

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

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

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

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

SEO に関する考慮事項

多くの場合、チームはウェブでのレンダリング戦略を選択する際に SEO の影響を考慮します。サーバーサイド レンダリングは通常、クローラーが簡単に解釈できる「完全な外観」のエクスペリエンスを提供するために選択されます。クローラーは JavaScript を理解している可能性がありますが、レンダリングの仕方で注意すべき制限事項があります。クライアントサイド レンダリングは機能しますが、多くの場合、追加のテストと手間のかかる作業が欠かせません。最近では、アーキテクチャがクライアントサイドの JavaScript に大きく依存している場合、ダイナミック レンダリングも検討に値する選択肢になっています。

選択した手法で期待どおりの結果が得られるかどうかをテストするうえで、モバイル フレンドリー テストツールは大変貴重です。プレビューには、Google のクローラーにページがどのように表示されるか、見つかったシリアル化された HTML コンテンツ(JavaScript の実行後)、レンダリング中に発生したエラーなどが表示されます。

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

まとめ

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

この記事で説明するオプションの範囲を示すインフォグラフィック。

クレジット

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

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