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

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

Google は、ここ数年間に Chrome で大規模なサイトと連携して得た知見に基づいて、この分野を理解しています。一般的に、完全なハイドレーション アプローチよりも、サーバーサイド レンダリングまたは静的レンダリングを検討することをおすすめします。

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

まず、使用する用語を定義します。

レンダリング

サーバーサイド レンダリング(SSR)
サーバー上でアプリをレンダリングして、JavaScript ではなく HTML をクライアントに送信する。
クライアントサイド レンダリング(CSR)
JavaScript を使用して DOM を変更し、ブラウザでアプリをレンダリングする。
リハイドレーション
クライアントで JavaScript ビューを「起動」して、サーバー レンダリングされた HTML の DOM ツリーとデータを再利用します。
事前レンダリング
ビルド時にクライアントサイド アプリケーションを実行して、初期状態を静的 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 費用を削減することで、残りの費用に予算を割り当てることができます。ただし、このアプローチには 1 つのトレードオフがあります。サーバー上でページを生成するのに時間がかかるため、ページの TTFB が増加する可能性があります。

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

多くの最新のフレームワーク、ライブラリ、アーキテクチャでは、クライアントとサーバーの両方で同じアプリケーションをレンダリングできます。これらの手法は、サーバーサイド レンダリングに使用できます。ただし、サーバークライアントの両方でレンダリングが行われるアーキテクチャは、パフォーマンス特性とトレードオフが大きく異なる独自のクラスのソリューションです。React ユーザーは、サーバーサイド レンダリングに サーバー DOM API または Next.js などのサーバー DOM API をベースにしたソリューションを使用できます。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 が無効になっているリンクなどの基本的な機能が残っている場合がありますが、ページのほとんどは動作しません。

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

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

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

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

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

サーバーサイド レンダリングは、PWA の構築時に興味深い判断を迫られることもあります。全ページのサービス ワーカー キャッシュを使用するか、個々のコンテンツをサーバー レンダリングするのか、どちらがよいでしょうか。

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

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

モバイル デバイスでクライアントサイド レンダリングを高速に保つのは難しい場合があります。JavaScript の予算を抑え、できるだけ少ないラウンドトリップで価値を提供するように少し調整すれば、クライアントサイド レンダリングで純粋なサーバーサイド レンダリングのパフォーマンスをほぼ再現できます。<link rel=preload> を使用して重要なスクリプトとデータを配信することで、パーサーの処理を高速化できます。また、PRPL などのパターンを使用して、最初のナビゲーションとそれ以降のナビゲーションを即時に感じるようにすることをおすすめします。

FCP と TTI に影響するクライアントサイド レンダリングを示す図。
クライアントサイド レンダリングを使用した FPC と 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 ドキュメント
HTML ドキュメント内にコードが重複している。

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

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

クライアント レンダリングが TTI に悪影響を及ぼしていることを示す図。
クライアントサイド レンダリングが TTI に与える影響。

ただし、サーバーサイド レンダリングとハイドレーションの組み合わせには希望があります。短期的には、キャッシュに保存しやすいコンテンツにのみサーバーサイド レンダリングを使用すると、TTFB を短縮し、プリレンダリングと同様の結果を得ることができます。段階的に、徐々に、または部分的に再水和することが、この手法を将来的により実用的にするための鍵となる可能性があります。

サーバーサイド レンダリングをストリーミングし、段階的に再水和する

サーバーサイド レンダリングはここ数年で多くの進歩を遂げています。

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

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

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

部分的な再水分補給

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

部分的なハイドレーション アプローチには、独自の問題と妥協点があります。キャッシュに保存する際には、いくつかの興味深い課題があります。クライアントサイド ナビゲーションでは、アプリケーションの非アクティブな部分のサーバー レンダリング HTML がページ全体を読み込まなくても利用可能であると想定できません。

トリスモルフィック レンダリング

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

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

SEO に関する考慮事項

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

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

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

まとめ

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

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

クレジット

レビューとインスピレーションを送ってくださった皆様、ありがとうございました。

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