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

ブラウザのプリロード スキャナの概要、パフォーマンスへの効果、スキャナを無効にする方法について説明します。

ページ速度の最適化で見落としがちな点として、ブラウザの内部についてある程度理解しておく必要があります。ブラウザは、デベロッパーが行えない方法でパフォーマンスを改善するために、特定の最適化を行います。ただし、その最適化が意図せず妨げられていない場合に限られます。

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

プリロード スキャナとは

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

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

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

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

また、defer 属性または async 属性のない <script> 要素が検出されると、ページの解析とレンダリングがブロックされます。

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

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

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

プリロード スキャナの役割は推測的です。つまり、元のマーカーアップを調べて、主な HTML パーサーが検出する前に、その場でフェッチするリソースを見つけます。

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

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

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

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

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

簡単な例を説明したので、次は、プリロード スキャナを回避できる実世界のパターンと、それを修正するためにできることを見てみましょう。

挿入された async スクリプト

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

<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 ネットワーク ウォーターフォール チャート。このページには、単一のスタイルシートと挿入された 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>

結果は次のとおりです。

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

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

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

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

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

答えは簡単です。起動時にスクリプトが必要である場合は、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 のダウンロード、コンパイル、実行が完了するまで遅延されます。

起動時にビューポート内にある遅延読み込み画像が、ブラウザのプリロード スキャナが画像リソースを見つけられず、遅延読み込みに必要な JavaScript が読み込まれて初めて読み込まれるため、必然的に遅延する様子を示した WebPageTest ネットワーク ウォーターフォール グラフ。画像が想定よりもはるかに遅れて検出される。
図 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 プロパティを持つ要素であるとします。リソースの読み込みは次のようになります。

background-image プロパティを使用して CSS から読み込まれた LCP 候補を含むページを示す WebPageTest ネットワーク ウォーターフォール グラフ。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 ヒントは小さいですが、ブラウザが画像をより早く検出できるようにします。

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

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

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

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

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

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

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

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

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

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

この例では、インライン化の影響により、LCP と全体的なパフォーマンスに悪影響が及んでいます。何もインライン化していないバージョンのページでは、LCP 画像が約 3.5 秒でペイントされます。すべてをインライン化するページでは、7 秒を過ぎるまで LCP 画像がペイントされません。

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

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

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

クライアントサイドの JavaScript によるマークアップのレンダリング

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

プリロード スキャナを回避できるパターンの 1 つは、クライアントサイドの JavaScript でマークアップをレンダリングすることです。

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

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

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

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

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

プリロード スキャナをサポートする

プリロード スキャナは、起動時にページをより速く読み込むのに役立つ、非常に効果的なブラウザの最適化機能です。重要なリソースを事前に検出できないパターンを避けることで、開発を簡素化するだけでなく、ウェブに関する主な指標など、多くの指標でより良い結果をもたらす優れたユーザー エクスペリエンスを実現できます。

ここまでの説明をまとめると、次のようになります。

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

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

リソース

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