JavaScript による HTML のレンダリングは、サーバーから送信された HTML のレンダリングとは異なり、パフォーマンスに影響する可能性があります。このガイドでは、これらの違いや、特にインタラクションが重要な場合のウェブサイトのレンダリング パフォーマンスを維持するための方法について説明します。
ブラウザに組み込まれたナビゲーション ロジック(「従来のページの読み込み」や「ハード ナビゲーション」と呼ばれることもあります)を使用するウェブサイトでは、HTML の解析とレンダリングは、デフォルトで非常に適切に行われます。このようなウェブサイトは、マルチページ アプリケーション(MPA)と呼ばれることもあります。
ただし、デベロッパーはアプリケーションのニーズに合わせてブラウザのデフォルト設定を調整できます。これは、シングルページ アプリケーション(SPA)パターンを使用するウェブサイトの場合に当てはまります。SPA パターンは、クライアント上に JavaScript を使用して HTML/DOM の大部分を動的に作成します。クライアントサイド レンダリングはこのデザイン パターンの名称であり、関連する作業が多すぎるとウェブサイトの Interaction to Next Paint(INP) に影響を与える可能性があります。
このガイドでは、サーバーからブラウザに送信される HTML を使用する場合と、クライアントで JavaScript を使用して HTML を作成する場合の違い、そして後者によって重要な場面でインタラクションのレイテンシが高くなる理由について説明します。
サーバーから提供された HTML をブラウザで表示する仕組み
従来のページ読み込みで使用されるナビゲーション パターンでは、ナビゲーションのたびにサーバーから HTML を受け取ります。ブラウザのアドレスバーに URL を入力するか、MPA のリンクをクリックすると、次の一連のイベントが発生します。
- ブラウザは、指定された URL へのナビゲーション リクエストを送信します。
- サーバーはチャンク形式で HTML を返します。
この最後のステップが重要です。これはサーバーとブラウザの交換における最も基本的なパフォーマンス最適化のひとつでもあり、ストリーミングと呼ばれています。サーバーが可能な限り早く HTML の送信を開始でき、ブラウザがレスポンス全体の到着を待たなければ、ブラウザは到着時に HTML をチャンク形式で処理できます。
ブラウザで行われるほとんどの処理と同様に、HTML の解析はタスク内で行われます。HTML がサーバーからブラウザにストリーミングされると、ストリームの一部がチャンク形式で到着するため、ブラウザは少しずつ解析することで HTML の解析を最適化します。その結果、ブラウザは各チャンクを処理した後に定期的にメインスレッドに譲歩するため、長いタスクを回避できます。つまり、HTML の解析中に、ページをユーザーに表示するために必要な増分レンダリング作業や、ページの重要な起動期間中に発生する可能性のあるユーザー操作の処理など、他の処理が行われる可能性があります。このアプローチは、ページの Interaction to Next Paint(INP) スコアの向上につながります。
つまり、サーバーから HTML をストリーミングすると、HTML の段階的な解析とレンダリング、メインスレッドへの自動放棄が無料で提供されます。これは、クライアントサイド レンダリングでは得られません。
ブラウザが JavaScript によって提供された HTML をレンダリングする仕組み
ページへのすべてのナビゲーション リクエストには、サーバーからある程度の HTML を提供する必要がありますが、一部のウェブサイトでは SPA パターンが使用されます。このアプローチでは多くの場合、サーバーによって提供される HTML の初期ペイロードが最小限に抑えられますが、クライアントはサーバーから取得したデータを基にした HTML をページのメイン コンテンツ領域に入力します。その後のナビゲーション(この場合は「ソフト ナビゲーション」と呼ばれることもあります)は、完全に JavaScript によって処理され、ページに新しい HTML が入力されます。
SPA 以外でも、JavaScript を介して HTML が動的に DOM に追加される一部のケースでは、クライアント側でレンダリングが行われることがあります。
JavaScript を使用して HTML を作成したり DOM に追加したりするには、次のような方法があります。
innerHTML
プロパティを使用すると、既存の要素のコンテンツを文字列で設定できます。ブラウザはこれを DOM に解析します。document.createElement
メソッドを使用すると、ブラウザの HTML 解析を使用せずに、DOM に追加する新しい要素を作成できます。document.write
メソッドを使用すると、ドキュメントに HTML を記述できます(方法 1 と同様に、ブラウザはそれを解析します)。ただし、さまざまな理由により、document.write
は使用しないことを強くおすすめします。
クライアントサイドの JavaScript で HTML/DOM を作成すると、次のような甚大な被害を受ける可能性があります。
- ナビゲーション リクエストに応じてサーバーによってストリーミングされる HTML とは異なり、クライアントの JavaScript タスクは自動的にチャンク化されないため、メインスレッドをブロックする長いタスクが発生する可能性があります。つまり、クライアント側で一度にあまりにも多くの HTML/DOM を作成すると、ページの INP に悪影響が及ぶ可能性があります。
- 起動時にクライアントで HTML が作成された場合、その HTML 内で参照されているリソースはブラウザのプリロード スキャナによって検出されません。これは確実にページの Largest Contentful Paint(LCP)に悪影響を及ぼします。これは実行時のパフォーマンスの問題ではなく(重要なリソースの取得におけるネットワーク遅延の問題ではありますが)、この根本的なブラウザ パフォーマンスの最適化が妨げられることでウェブサイトの LCP が影響を受けることは望ましくありません。
クライアントサイド レンダリングによるパフォーマンスへの影響に対処する方法
ウェブサイトがクライアントサイドのレンダリングに大きく依存しており、フィールド データの INP 値が低いことが確認された場合、クライアントサイド レンダリングが問題に関係しているかどうかを疑問に思われるかもしれません。たとえば、SPA のウェブサイトの場合、フィールド データから、レンダリング処理に多くの時間がかかっているインタラクションが明らかになることがあります。
いずれにしても、原因が何であれ、本題に戻すために考えられる原因を以下にご紹介します。
サーバーからできるだけ多くの HTML を提供する
前述のとおり、ブラウザはデフォルトでサーバーからの HTML を非常に効率的に処理します。HTML の解析とレンダリングを分割して長いタスクを回避し、メインスレッドの合計時間を最適化します。これにより、Total Blocking Time(TBT)が短縮され、TBT は INP と強く相関しています。
ウェブサイトの構築にフロントエンド フレームワークを使用しているかもしれません。サーバーでコンポーネントの HTML をレンダリングしていることを確認する必要があります。これにより、ウェブサイトで必要になる最初のクライアントサイド レンダリングの量が制限され、エクスペリエンスが向上します。
- React の場合は、Server DOM API を使用してサーバー上で HTML をレンダリングします。ただし、サーバー側のレンダリングの従来の方法では同期アプローチを使用しているため、最初のバイトまでの時間(TTFB)や、First Contentful Paint(FCP)や LCP などの指標が長くなる可能性があります。可能であれば、Node.js またはその他の JavaScript ランタイムのストリーミング API を使用して、サーバーがブラウザへの HTML のストリーミングをできるだけ早く開始できるようにしてください。Next.js(React ベースのフレームワーク)には、多くのベスト プラクティスがデフォルトで用意されています。サーバーで HTML を自動的にレンダリングするだけでなく、ユーザーのコンテキスト(認証など)に基づいて変更されないページの HTML を静的に生成することもできます。
- Vue はデフォルトでクライアントサイド レンダリングも行います。ただし、React と同様に、Vue ではコンポーネント HTML をサーバーでレンダリングすることもできます。可能であれば、これらのサーバーサイド API を活用するか、ベスト プラクティスを簡単に実装できるように Vue プロジェクトでより高度な抽象化を検討してください。
- Svelte はデフォルトでサーバーで HTML をレンダリングしますが、コンポーネントのコードがブラウザ専用の名前空間(
window
など)にアクセスする必要がある場合、そのコンポーネントの HTML をサーバーでレンダリングできないことがあります。可能であれば別の方法を検討し、不要なクライアントサイド レンダリングを引き起こさないようにします。SvelteKit(Next.js のように Svelte と React)では、Svelte プロジェクトに可能な限り多くのベスト プラクティスが埋め込まれているため、Svelte のみを使用するプロジェクトで発生する潜在的な問題を回避できます。
クライアントで作成される DOM ノードの数を制限する
DOM が大きいと、レンダリングに必要な処理時間が長くなる傾向があります。ウェブサイトが本格的な SPA であっても、MPA とのインタラクションの結果として既存の DOM に新しいノードを挿入する場合でも、それらの DOM をできる限り小さくすることを検討してください。これにより、クライアントサイド レンダリングで HTML を表示するために必要な作業が減り、ウェブサイトの INP を低く抑えることができます。
ストリーミング Service Worker アーキテクチャを検討する
これは高度な手法であり、あらゆるユースケースで簡単にはうまくいかないかもしれませんが、ユーザーがページ間を移動したときに、瞬時に読み込まれるように MPA をウェブサイトに変えることができます。Service Worker を使用して CacheStorage
でウェブサイトの静的部分を事前キャッシュし、ReadableStream
API を使用して残りのページの HTML をサーバーから取得することができます。
このテクニックを使いこなせば、クライアントで HTML を作成しているとは言えませんが、キャッシュからコンテンツの一部が瞬時に読み込まれると、サイトの読み込みが速いという印象を与えることになります。このアプローチを使用しているウェブサイトは、SPA のように感じられますが、クライアントサイド レンダリングの問題はありません。また、サーバーにリクエストする HTML の量も削減できます。
つまり、ストリーミング Service Worker のアーキテクチャは、ブラウザに組み込まれたナビゲーション ロジックを置き換えるものではなく、ロジックを追加するものです。Workbox でこれを実現する方法について詳しくは、ストリームによるマルチページ アプリケーションの高速化をご覧ください。
まとめ
ウェブサイトがどのように HTML を受信してレンダリングするかは、パフォーマンスに影響します。ウェブサイトを機能させるために必要な HTML のすべて(または大量)の送信をサーバーに依存させると、増分解析とレンダリング、そして時間のかかるタスクを回避するためにメインスレッドに自動的に譲るといった多くのことを無料で得ることができます。
クライアントサイドの HTML レンダリングでは、多くの潜在的なパフォーマンスの問題が生じますが、これは多くの場合、回避可能です。ただし、個々のウェブサイトの要件により、常に完全に回避できるわけではありません。クライアント サイトの過剰なレンダリングによって生じる可能性のある時間のかかるタスクを軽減するには、ウェブサイトの HTML を可能な限りサーバーから送信し、クライアントでレンダリングする必要がある HTML の DOM サイズをできる限り小さくします。また、サーバーから読み込まれる HTML に対してブラウザが提供する段階的な解析とレンダリングを活用しながら、クライアントへの HTML の配信を高速化する代替アーキテクチャを検討してください。
ウェブサイトのクライアントサイド レンダリングをできるだけ最小限に抑えることができれば、ウェブサイトの INP だけでなく、LCP、TBT、場合によっては TTFB などの他の指標も改善されます。
Maik Jonietz による Unsplash のヒーロー画像。