ブラウザのプリロード スキャナに対抗しない

ブラウザのプリロード スキャナとは何か、パフォーマンスにどのように役立つか、どのようにすればスキャナの邪魔にならないかについて説明します。

Page Speed の最適化で軽視されがちなのは、ブラウザの内部構造について少し知っておくことです。ブラウザは、デベロッパーができない方法でパフォーマンスを向上させるために特定の最適化を行いますが、その最適化が意図せずに妨げられない場合に限ります。

理解しておくべき内部ブラウザの最適化の 1 つに、ブラウザのプリロード スキャナがあります。この記事では、プリロード スキャナの仕組みと、プリロード スキャナの妨げにならないようにする方法について説明します。

プリロード スキャナとは

すべてのブラウザには、生のマークアップをトークン化してオブジェクト モデルに処理するプライマリ HTML パーサーがあります。この処理は、<link> 要素で読み込まれたスタイルシートや、async 属性または defer 属性のない <script> 要素で読み込まれたスクリプトなどのブロック リソースが見つかるまで続きます。

HTML パーサーの図。
図 1: ブラウザのメイン HTML パーサーをブロックする方法の図。この場合、パーサーは外部 CSS ファイルの <link> 要素に遭遇し、CSS がダウンロードされて解析されるまで、ブラウザがドキュメントの残りの部分を解析したり、レンダリングしたりするのをブロックします。

CSS ファイルの場合、スタイルなしコンテンツのフラッシュ(FOUC)を防ぐためにレンダリングがブロックされます。FOUC とは、スタイルが適用される前に、スタイルなしのページが一瞬表示されることです。

スタイルが適用されていない状態(左)とスタイルが適用されている状態(右)の web.dev のホームページ。
図 2: FOUC のシミュレートされた例。左はスタイルなしの web.dev のフロントページです。右側は、スタイルが適用された同じページです。スタイルなしの状態は、スタイルシートのダウンロードと処理中にブラウザがレンダリングをブロックしない場合に、一瞬発生することがあります。

ブラウザは、defer 属性または async 属性のない <script> 要素を検出した場合も、ページの解析とレンダリングをブロックします。

これは、プライマリ HTML パーサーがまだ処理を行っている間に、特定のスクリプトが DOM を変更するかどうかをブラウザが確実に判断できないためです。このため、ブロックされた解析とレンダリングの影響を最小限に抑えるために、ドキュメントの最後に JavaScript を読み込むのが一般的です。

これらは、ブラウザが解析とレンダリングの両方をブロックすべき妥当な理由です。ただし、これらの重要な手順のいずれかをブロックすると、他の重要なリソースの検出が遅れてショーが遅延する可能性があるため、望ましくありません。幸いなことに、ブラウザはプリロード スキャナと呼ばれるセカンダリ HTML パーサーを使用して、これらの問題を軽減するよう努めています。

プライマリ HTML パーサー(左)とプリロード スキャナ(右)の両方の図。プリロード スキャナはセカンダリ HTML パーサーです。
図 3: プリロード スキャナがメインの HTML パーサーと並行して動作し、投機的にアセットを読み込む仕組みを示した図。この場合、プライマリ HTML パーサーは、<body> 要素内の画像マークアップの処理を開始する前に CSS を読み込んで処理するため、ブロックされますが、プリロード スキャナは未加工のマークアップを先読みして画像リソースを見つけ、プライマリ HTML パーサーがブロック解除される前に読み込みを開始できます。

プリロード スキャナの役割は推測的です。つまり、生のマークアップを調べて、メインの HTML パーサーが検出する前に取得できるリソースを見つけます。

プリロード スキャナが動作しているかどうかを確認する方法

プリロード スキャナは、レンダリングと解析がブロックされているために存在します。この 2 つのパフォーマンスの問題がなければ、プリロード スキャナはあまり役に立ちません。ウェブページがプリロード スキャナの恩恵を受けられるかどうかを判断する鍵は、これらのブロック現象にあります。そのためには、リクエストに人工的な遅延を導入して、プリロード スキャナがどこで動作しているかを確認します。

スタイルシートを含む基本的なテキストと画像のこのページを例に取ります。CSS ファイルはレンダリングと解析の両方をブロックするため、プロキシ サービスを介してスタイルシートに 2 秒の人工的な遅延を導入します。この遅延により、ネットワーク ウォーターフォールでプリロード スキャナが動作している場所を簡単に確認できます。

WebPageTest のネットワーク ウォーターフォール グラフは、スタイルシートに 2 秒の人工的な遅延が課せられていることを示しています。
図 4: モバイル デバイスの Chrome でシミュレートされた 3G 接続で実行されたウェブページWebPageTest ネットワーク ウォーターフォール チャート。スタイルシートは、読み込みが開始される前にプロキシによって 2 秒間遅延されますが、マークアップ ペイロードの後ろにある画像はプリロード スキャナによって検出されます。

ウォーターフォールでわかるように、プリロード スキャナは、レンダリングとドキュメントの解析がブロックされている場合でも<img> 要素を検出します。この最適化がないと、ブラウザはブロック期間中に機会的に取得できず、リソース リクエストが同時ではなく連続して行われることになります。

おもちゃの例はこれで終わりにして、プリロード スキャナを回避できる実際のパターンと、それらを修正する方法を見ていきましょう。

挿入された async スクリプト

<head> に次のようなインライン JavaScript を含む HTML があるとします。

<script>
  const scriptEl = document.createElement('script');
  scriptEl.src = '/yall.min.js';

  document.head.appendChild(scriptEl);
</script>

挿入されたスクリプトはデフォルトで async になるため、このスクリプトが挿入されると、async 属性が適用されたかのように動作します。つまり、できるだけ早く実行され、レンダリングをブロックしません。最適ですよね。ただし、このインライン <script> が外部 CSS ファイルを読み込む <link> 要素の後に来ると仮定すると、次のような最適でない結果になります。

この WebPageTest のグラフは、スクリプトが挿入されたときにプリロード スキャンが失敗したことを示しています。
図 5: モバイル デバイスの Chrome でシミュレートされた 3G 接続を介して実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。このページには、1 つのスタイルシートと挿入された async スクリプトが含まれています。プリロード スキャナは、クライアントに挿入されたスクリプトをレンダリング ブロック フェーズで検出できません。

ここで何が起きたのかを詳しく見てみましょう。

  1. 0 秒でメイン ドキュメントがリクエストされます。
  2. 1.4 秒で、ナビゲーション リクエストの最初のバイトが到着します。
  3. 2.0 秒で、CSS と画像がリクエストされます。
  4. パーサーがスタイルシートの読み込みをブロックし、async スクリプトを挿入するインライン JavaScript がそのスタイルシートの 2.6 秒後に実行されるため、そのスクリプトが提供する機能は、可能な限り早く利用できるようにはなりません。

スタイルシートのダウンロードが完了した後にのみスクリプトのリクエストが発生するため、これは最適ではありません。これにより、スクリプトの実行をできるだけ遅らせることができます。一方、<img> 要素はサーバー提供のマークアップで検出可能であるため、プリロード スキャナによって検出されます。

では、スクリプトを DOM に挿入するのではなく、async 属性を持つ通常の <script> タグを使用するとどうなるでしょうか?

<script src="/yall.min.js" async></script>

結果は次のとおりです。

WebPageTest のネットワーク ウォーターフォール。HTML スクリプト要素を使用して読み込まれた非同期スクリプトが、ブラウザのメインの HTML パーサーがスタイルシートのダウンロードと処理中にブロックされている場合でも、ブラウザのプリロード スキャナによって検出される様子を示しています。
図 6: モバイル デバイスの Chrome でシミュレートされた 3G 接続で実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。このページには、1 つのスタイルシートと 1 つの async <script> 要素が含まれています。プリロード スキャナはレンダリング ブロック フェーズでスクリプトを検出し、CSS と同時に読み込みます。

これらの問題は rel=preload を使用することで解決できると提案したくなるかもしれませんが、この方法は確かに有効ですが、副作用が生じる可能性があります。結局のところ、<script> 要素を DOM に挿入しないことで回避できる問題を、なぜ rel=preload を使用して解決する必要があるのでしょうか?

WebPageTest のウォーターフォール。rel=preload リソースヒントを使用して、非同期で挿入されたスクリプトの検出を促進する方法を示しています。ただし、意図しない副作用が生じる可能性があります。
図 7: モバイル デバイスの Chrome でシミュレートされた 3G 接続で実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。このページには、単一のスタイルシートと挿入された async スクリプトが含まれていますが、async スクリプトはより早く検出されるようにプリロードされています。

プリロードはここで問題を「解決」しますが、新しい問題が発生します。最初の 2 つのデモの async スクリプトは <head> で読み込まれているにもかかわらず、優先度が「低」で読み込まれ、スタイルシートは優先度が「最高」で読み込まれます。async スクリプトがプリロードされる最後のデモでは、スタイルシートは引き続き「Highest」の優先度で読み込まれますが、スクリプトの優先度は「High」に引き上げられています。

リソースの優先度を上げると、ブラウザはより多くの帯域幅を割り当てます。つまり、スタイルシートの優先度が最も高い場合でも、スクリプトの優先度が高くなると帯域幅の競合が発生する可能性があります。これは、接続が遅い場合や、リソースが非常に大きい場合に要因となる可能性があります。

答えは簡単です。起動時にスクリプトが必要な場合は、DOM に挿入してプリロード スキャナを無効にしないでください。必要に応じて、<script> 要素の配置や、deferasync などの属性をテストします。

JavaScript を使用した遅延読み込み

遅延読み込みは、データを節約する優れた方法であり、画像によく適用されます。ただし、遅延読み込みが「スクロールせずに見える範囲」の画像に誤って適用されることがあります。

これにより、プリロード スキャナに関するリソースの検出可能性に問題が生じる可能性があり、画像への参照の検出、ダウンロード、デコード、表示にかかる時間が不必要に長くなる可能性があります。たとえば、次のような画像マークアップがあるとします。

<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">

data- 接頭辞の使用は、JavaScript を使用した遅延読み込みでよく見られるパターンです。画像がビューポートにスクロールされると、遅延読み込みツールは data- 接頭辞を削除します。つまり、上記の例では data-srcsrc になります。この更新により、ブラウザはリソースを取得します。

このパターンは、起動時にビューポート内の画像に適用されるまでは問題ありません。プリロード スキャナは src(または srcset)属性と同じ方法で data-src 属性を読み取らないため、画像参照は早期に検出されません。さらに、画像は遅延読み込み JavaScript のダウンロード、コンパイル、実行が完了するまで読み込まれません。

WebPageTest のネットワーク ウォーターフォール チャート。起動時にビューポート内にある遅延読み込みされた画像が、ブラウザのプリロード スキャナが画像リソースを見つけられず、遅延読み込みに必要な JavaScript が読み込まれたときにのみ読み込まれるため、遅延せざるを得ないことを示しています。画像が本来よりもはるかに遅れて検出される。
図 8: モバイル デバイスの Chrome でシミュレートされた 3G 接続を介して実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。起動時にビューポートに表示されるにもかかわらず、画像リソースが不必要に遅延読み込みされています。これにより、プリロード スキャナが無効になり、不要な遅延が発生します。

画像のサイズ(ビューポートのサイズによって異なる場合があります)によっては、Largest Contentful Paint(LCP)の候補要素になることがあります。プリロード スキャナが投機的に画像リソースを事前に取得できない場合(ページのスタイルシートがレンダリングをブロックしている可能性がある場合など)、LCP が低下します。

解決策は、画像マークアップを変更することです。

<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">

これは、起動時にビューポートに表示される画像に最適なパターンです。プリロード スキャナが画像リソースをより迅速に検出して取得するためです。

起動時にビューポート内の画像を読み込むシナリオを示す WebPageTest のネットワーク ウォーターフォール チャート。画像は遅延読み込みされないため、読み込みにスクリプトを必要としません。つまり、プリロード スキャナがより早く検出できます。
図 9: モバイル デバイスの Chrome でシミュレートされた 3G 接続で実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。プリロード スキャナは、CSS と JavaScript の読み込みが開始される前に画像リソースを検出するため、ブラウザは画像リソースの読み込みを早めに開始できます。

この簡略化された例では、接続が遅い場合の LCP が 100 ミリ秒改善されています。大きな改善ではないように見えるかもしれませんが、このソリューションはマークアップの簡単な修正であり、ほとんどのウェブページはこの例よりも複雑であることを考えると、大きな改善です。つまり、LCP 候補は他の多くのリソースと帯域幅を競合する可能性があるため、このような最適化がますます重要になります。

CSS の背景画像

ブラウザのプリロード スキャナはマークアップをスキャンします。background-image プロパティで参照される画像の取得を伴う可能性がある CSS など、他のリソースタイプはスキャンされません。

HTML と同様に、ブラウザは CSS を独自のオブジェクト モデル(CSSOM)に処理します。CSSOM の構築中に外部リソースが検出された場合、それらのリソースはプリロード スキャナではなく、検出時にリクエストされます。

ページの LCP 候補が CSS の background-image プロパティを持つ要素であるとします。リソースの読み込み時に発生する処理は次のとおりです。

WebPageTest のネットワーク ウォーターフォール チャート。CSS の background-image プロパティを使用して読み込まれた LCP 候補を含むページが描かれている。LCP 候補の画像がブラウザのプリロード スキャナで検査できないリソースタイプであるため、CSS がダウンロードされて処理されるまでリソースの読み込みが遅延し、LCP 候補のペイント時間が遅延します。
図 10: モバイル デバイスの Chrome でシミュレートされた 3G 接続で実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。ページの LCP 候補は、CSS の background-image プロパティを持つ要素です(3 行目)。リクエストされた画像は、CSS パーサーがその画像を見つけるまでフェッチを開始しません。

この場合、プリロード スキャナは無効化されるというより、関与しないことになります。ただし、ページの LCP 候補が background-image CSS プロパティからのものである場合は、その画像をプリロードする必要があります。

<!-- Make sure this is in the <head> below any
     stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">

この rel=preload ヒントは小さいですが、ブラウザが画像を通常よりも早く検出するのに役立ちます。

WebPageTest のネットワーク ウォーターフォール チャート。rel=preload ヒントの使用により、CSS の背景画像(LCP 候補)の読み込みが大幅に早くなっています。LCP 時間が約 250 ミリ秒短縮されます。
図 11: モバイル デバイスの Chrome でシミュレートされた 3G 接続を介して実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。ページの LCP 候補は、CSS の background-image プロパティを持つ要素です(3 行目)。rel=preload ヒントを使用すると、ブラウザで画像を検出するタイミングがヒントなしの場合よりも約 250 ミリ秒早くなります。

rel=preload ヒントを使用すると、LCP 候補がより早く検出され、LCP 時間が短縮されます。このヒントは問題を解決するのに役立ちますが、画像 LCP 候補を CSS から読み込む必要があるかどうかを評価する方が良い場合もあります。<img> タグを使用すると、プリロード スキャナで検出可能な状態を維持しながら、ビューポートに適した画像を読み込む処理をより細かく制御できます。

インライン化するリソースが多すぎる

インライン化とは、リソースを HTML 内に配置する手法です。<style> 要素のスタイルシート、<script> 要素のスクリプト、その他のほぼすべてのリソースを base64 エンコードを使用してインライン化できます。

リソースをインライン化すると、リソースに対して個別のリクエストが発行されないため、ダウンロードするよりも高速になることがあります。ドキュメント内に表示され、すぐに読み込まれます。ただし、次のような大きな欠点があります。

  • HTML をキャッシュに保存していない場合(HTML レスポンスが動的な場合はキャッシュに保存できません)、インライン リソースはキャッシュに保存されません。インライン リソースは再利用できないため、パフォーマンスに影響します。
  • HTML をキャッシュに保存できても、インライン リソースはドキュメント間で共有されません。これにより、オリジン全体でキャッシュに保存して再利用できる外部ファイルと比較して、キャッシュ保存の効率が低下します。
  • インライン化しすぎると、余分なインライン コンテンツのダウンロードに時間がかかるため、プリロード スキャナがドキュメント内のリソースを検出するタイミングが遅くなります。

このページを例に説明します。特定の条件では、LCP 候補はページ上部の画像であり、CSS は <link> 要素によって読み込まれる別のファイルにあります。このページでは、CSS リソースから個別のファイルとしてリクエストされる 4 つのウェブフォントも使用されています。

外部 CSS ファイルを含むページの WebPageTest ネットワーク ウォーターフォール チャート。このファイルには 4 つのフォントが参照されています。LCP 候補の画像は、プリロード スキャナによって適切なタイミングで検出されます。
図 12: モバイル デバイスの Chrome でシミュレートされた 3G 接続で実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。ページの LCP 候補は <img> 要素から読み込まれた画像ですが、ページの読み込みに必要な CSS とフォントが別々のリソースで読み込まれるため、プリロード スキャナの動作が遅れることはなく、プリロード スキャナによって検出されます。

では、CSS すべてのフォントが base64 リソースとしてインライン化されている場合はどうなるでしょうか?

外部 CSS ファイルを含むページの WebPageTest ネットワーク ウォーターフォール チャート。このファイルには 4 つのフォントが参照されています。プリロード スキャナが LCP 画像の検出を大幅に遅らせている。
図 13: モバイル デバイスの Chrome でシミュレートされた 3G 接続で実行されたウェブページの WebPageTest ネットワーク ウォーターフォール チャート。ページの LCP 候補は <img> 要素から読み込まれた画像ですが、CSS とその 4 つのフォント リソースのインライン化により、これらのリソースが完全にダウンロードされるまでプリロード スキャナが画像を検出できません。

この例では、インライン化の影響により、LCP とパフォーマンス全般に悪影響が生じています。インライン化されていないバージョンのページでは、LCP 画像のレンダリングに約 3.5 秒かかります。すべてをインライン化するページでは、LCP 画像が 7 秒強まで描画されません。

ここでは、プリロード スキャナ以外の要素も関係しています。base64 はバイナリ リソースの非効率的な形式であるため、フォントのインライン化は優れた戦略ではありません。また、外部フォント リソースは、CSSOM によって必要と判断されない限りダウンロードされないという要因もあります。これらのフォントが base64 としてインライン化されると、現在のページで必要かどうかに関係なくダウンロードされます。

プリロードでこの問題を改善できますか?いいね。LCP 画像をプリロードして LCP 時間を短縮することは可能ですが、インライン リソースでキャッシュに保存できない可能性のある HTML を肥大化させると、パフォーマンスに悪影響を及ぼす可能性があります。First Contentful Paint(FCP)もこのパターンに影響されます。何もインライン化されていないバージョンのページの FCP は約 2.7 秒です。すべてがインライン化されたバージョンでは、FCP は約 5.8 秒です。

特に base64 でエンコードされたリソースなど、HTML にインライン化する際は十分に注意してください。通常はおすすめしません(非常に小さなリソースを除く)。インライン化はできるだけ少なくします。インライン化しすぎると危険です。

クライアントサイドの JavaScript を使用したマークアップのレンダリング

JavaScript がページの読み込み速度に影響することは間違いありません。デベロッパーはインタラクティブな機能を提供するために JavaScript を利用するだけでなく、コンテンツ自体を配信するために JavaScript を利用する傾向もあります。これにより、デベロッパー エクスペリエンスが向上する面もありますが、デベロッパーにとってのメリットが必ずしもユーザーにとってのメリットにつながるとは限りません。

プリロード スキャナを回避できるパターンの一つに、クライアントサイドの JavaScript でマークアップをレンダリングする方法があります。

WebPageTest のネットワーク ウォーターフォール。画像とテキストを含む基本的なページが JavaScript でクライアントに完全にレンダリングされていることを示しています。マークアップが JavaScript 内に含まれているため、プリロード スキャナはリソースを検出できません。JavaScript フレームワークで必要なネットワークと処理時間の増加により、すべてのリソースの遅延がさらに大きくなります。
図 14: モバイル デバイスの Chrome でシミュレートされた 3G 接続を介して実行されたクライアントサイド レンダリングのウェブページの WebPageTest ネットワーク ウォーターフォール チャート。コンテンツが JavaScript に含まれており、レンダリングにフレームワークを使用しているため、クライアント レンダリングされたマークアップの画像リソースはプリロード スキャナから隠されています。サーバーサイド レンダリングによる同等のエクスペリエンスを図 9 に示します。

マークアップ ペイロードがブラウザの JavaScript に含まれて完全にレンダリングされる場合、そのマークアップ内のリソースはプリロード スキャナから実質的に見えなくなります。これにより、重要なリソースの検出が遅延し、LCP に影響します。これらの例では、LCP 画像のリクエストは、JavaScript を必要としない同等のサーバー レンダリング エクスペリエンスと比較して、大幅に遅延しています。

この記事の焦点から少し外れますが、クライアントでのマークアップのレンダリングの影響は、プリロード スキャナの無効化をはるかに超えるものです。たとえば、JavaScript を導入して、JavaScript を必要としないエクスペリエンスを実現すると、不要な処理時間が生じ、Interaction to Next Paint(INP)に影響する可能性があります。クライアントで大量のマークアップをレンダリングすると、サーバーから同じ量のマークアップが送信される場合と比較して、長いタスクが発生する可能性が高くなります。この理由は、JavaScript に伴う追加の処理以外にも、ブラウザがサーバーからマークアップをストリーミングし、レンダリングをチャンク化して長いタスクを制限する傾向があるためです。一方、クライアント レンダリングされたマークアップは単一のモノリシック タスクとして処理されるため、ページの INP に影響する可能性があります。

このシナリオの解決策は、ページマークアップをクライアントでレンダリングするのではなく、サーバーで提供できない理由があるか?という質問への回答によって異なります。この答えが「いいえ」の場合、可能な限りサーバーサイド レンダリング(SSR)または静的に生成されたマークアップを検討する必要があります。これにより、プリロード スキャナが重要なリソースを事前に検出して取得できるようになります。

ページの一部のマークアップに機能を付加するために JavaScript が必要な場合でも、SSR を使用して、プレーンな JavaScript またはハイドレーションで両方のメリットを活かすことができます。

プリロード スキャナを有効にする

プリロード スキャナは、起動時にページの読み込みを高速化する非常に効果的なブラウザ最適化です。重要なリソースを事前に検出する機能を損なうパターンを避けることで、開発が簡単になるだけでなく、ユーザー エクスペリエンスが向上し、ウェブに関する主な指標など、多くの指標でより良い結果が得られます。

まとめると、この投稿から次のことを学んでいただきたいです。

  • ブラウザのプリロード スキャナは、プライマリ HTML パーサーがブロックされた場合に、その前にスキャンして、より早く取得できるリソースを機会的に検出するセカンダリ HTML パーサーです。
  • 最初のナビゲーション リクエストでサーバーから提供されたマークアップに存在しないリソースは、プリロード スキャナで検出できません。プリロード スキャナを回避する方法には、次のようなものがあります(これらに限定されません)。
    • JavaScript を使用して DOM にリソースを挿入する。スクリプト、画像、スタイルシートなど、サーバーからの初期マークアップ ペイロードに含めた方がよいもの。
    • JavaScript ソリューションを使用して、ファーストビューの画像または iframe を遅延読み込みします。
    • JavaScript を使用してドキュメント サブリソースへの参照を含む可能性のあるマークアップをクライアントでレンダリングする。
  • プリロード スキャナは HTML のみをスキャンします。他のリソース(特に CSS)の内容は検査されません。これらのリソースには、LCP 候補などの重要なアセットへの参照が含まれている可能性があります。

なんらかの理由で、プリロード スキャナの読み込みパフォーマンスを高速化する能力に悪影響を及ぼすパターンを回避できない場合は、rel=preload リソースヒントを検討してください。rel=preload使用する場合は、ラボツールでテストして、目的の効果が得られることを確認してください。最後に、プリロードするリソースが多すぎると、すべてが優先順位付けされるため、何も優先順位付けされなくなります。

リソース

Unsplash のヒーロー画像(Mohammad Rahmani 撮影)。