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

ブラウザのプリロード スキャナの概要、パフォーマンス向上の仕組み、対策についてご紹介します。

ページ速度の最適化において見落とされがちな側面の一つに、ブラウザ内部の知識があります。ブラウザは、デベロッパーにはできない方法でパフォーマンスを改善するために特定の最適化を行います。ただし、その最適化が意図せず妨げられない限りは例外です。

内部でブラウザを最適化する方法の 1 つに、ブラウザのプリロード スキャナがあります。この投稿では、プリロード スキャナの仕組みと、さらに重要な点として、プリロード スキャナの妨げを回避する方法について説明します。

プリロード スキャナとは

どのブラウザにも、未加工のマークアップをトークン化してオブジェクト モデルに変換するメインの 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 パーサーと並行して動作し、投機的にアセットを読み込む仕組みを示す図。この例では、プライマリ HTML パーサーは、<body> 要素内の画像マークアップの処理を開始する前に、CSS の読み込みと処理を行うためブロックされますが、プリロード スキャナは未加工のマークアップを調べて画像リソースを探し、プライマリ HTML パーサーのブロックが解除される前に読み込みを開始します。

プリロード スキャナの役割は投機的です。つまり、プリロード スキャナは未加工のマークアップを調べて、プライマリ HTML パーサーがリソースを検出する前に、便宜的にフェッチするリソースを見つけます。

プリロード スキャナの動作を確認する方法

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

例として、スタイルシートを含む基本的なテキストと画像のこちらのページをご覧ください。CSS ファイルはレンダリングと解析のどちらもブロックするため、プロキシ サービスを介したスタイルシートの表示に 2 秒の人為的な遅延が発生します。この遅延により、プリロード スキャナが動作しているネットワーク ウォーターフォールでより簡単に確認できるようになります。

WebPageTest ネットワークのウォーターフォール グラフでは、スタイルシートに 2 秒の人為的な遅延が設定されています。
図 4: シミュレートされた 3G 接続を介して、モバイル デバイスの Chrome 上で実行されたウェブページ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: シミュレートされた 3G 接続を介してモバイル デバイスの Chrome で実行されるウェブページの 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: シミュレートされた 3G 接続を介してモバイル デバイスの Chrome で実行されるウェブページの WebPageTest ネットワーク ウォーターフォール チャート。このページには、1 つのスタイルシートと 1 つの async <script> 要素が含まれています。プリロード スキャナは、レンダリング ブロック フェーズでスクリプトを検出し、CSS と同時に読み込みます。

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

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

プリロードを行うと問題は「修正」されますが、新たな問題が発生します。最初の 2 つのデモの async スクリプトは(<head> で読み込まれているにもかかわらず)優先度「低」で読み込まれ、スタイルシートは「最高」の優先度で読み込まれます。async スクリプトをプリロードする直前のデモでは、スタイルシートは引き続き「最高」優先度で読み込まれますが、スクリプトの優先度は「High」に昇格されています。

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

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

JavaScript による遅延読み込み

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

これにより、プリロード スキャナが関係するリソースの検出可能性に関する潜在的な問題が生じ、画像への参照の検出、ダウンロード、デコード、表示にかかる時間が不必要に遅延する可能性があります。この画像マークアップの例を見てみましょう。

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

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

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

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

画像のサイズ(ビューポートのサイズにもよります)によっては、Largest Contentful Paint(LCP)要素の候補となる場合があります。プリロード スキャナが事前に画像リソースを投機的にフェッチできない場合(おそらくページのスタイルシートでレンダリングがブロックされる時点)、LCP は問題が発生します。

この問題を解決するには、画像のマークアップを変更します。

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

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

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

この簡素化された例の結果では、低速の接続でも LCP が 100 ミリ秒改善されています。それほど大きな改善に思えないかもしれませんが、解決策は簡単なマークアップ修正であり、ほとんどのウェブページはこの例よりも複雑だと考えてみてください。つまり、LCP の候補は他の多くのリソースと帯域幅を競い合う必要があるため、このような最適化はますます重要になります。

CSS 背景画像

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

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

たとえば、ページの LCP 候補が CSS の background-image プロパティを持つ要素だとします。リソースが読み込まれると、次のようになります。

背景画像プロパティを使用して CSS から LCP 候補を読み込んだページを示す WebPageTest ネットワーク ウォーターフォール グラフ。LCP 候補の画像は、ブラウザ プリロード スキャナが検査できないリソースタイプであるため、CSS がダウンロードされて処理されるまでリソースの読み込みが遅延し、LCP 候補のペイント時間が遅くなります。
図 10: シミュレートされた 3G 接続を介してモバイル デバイスの Chrome で実行されるウェブページの 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: シミュレートされた 3G 接続を介してモバイル デバイスの Chrome で実行されるウェブページの 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> 要素によって読み込まれた別のファイルにあります。このページでは 4 つのウェブフォントも使用しています。これらのウェブフォントは、CSS リソースとは別のファイルとしてリクエストされます。

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

CSS とすべてのフォントを base64 リソースとしてインライン化するとどうなるでしょうか。

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

インライン化の影響は、この例の LCP に悪影響を及ぼしますが、パフォーマンス全般に悪影響を及ぼします。何もインライン化していないバージョンのページでは、約 3.5 秒で LCP 画像が描画されます。すべてをインライン化するページでは、LCP 画像は 7 秒強まで表示されません。

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

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

HTML へのインライン化、特に base64 でエンコードされたリソースには細心の注意を払ってください。リソースが限られている場合を除き、通常はおすすめしません。インライン化しすぎると火花を散らすものなので、できるだけインライン化してください。

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

JavaScript はページ速度に確実に影響します。開発者はインタラクティビティを提供するだけでなく、コンテンツ自体を配信するためにも BigQuery に依存する傾向があります。これはある意味でデベロッパー エクスペリエンスの向上につながりますが、デベロッパーにとってのメリットが必ずしもユーザーにメリットをもたらすとは限りません。

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

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

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

これはこの記事の要点から少し外れていますが、クライアントに対するマークアップのレンダリングの影響は、プリロード スキャナを無効化する以上のものです。まず、JavaScript を導入して必要のないエクスペリエンスを実現すると、不要な処理時間が発生し、Interaction to Next Paint(INP)に影響する可能性があります。

また、クライアントで大量のマークアップをレンダリングすると、サーバーで送信される同じ量のマークアップに比べ、処理に時間がかかるタスクが発生する可能性が高くなります。その理由は、JavaScript の追加処理とは別に、ブラウザがサーバーからマークアップをストリーミングし、長いタスクを回避するようにレンダリングをチャンク化するためです。一方、クライアントがレンダリングするマークアップは単一のモノリシック タスクとして扱われるため、INP に加えて合計ブロック時間(TBT)初回入力遅延(FID)などのページの応答性の指標に影響する可能性があります。

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

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

プリロード スキャナの活用

プリロード スキャナはブラウザの最適化に非常に有効で、起動時のページの読み込みを高速化できます。重要なリソースを前もって見つける能力を損なうパターンを避けることで、開発をシンプルにできるだけでなく、ウェブに関する指標を含む多くの指標で優れた結果をもたらす優れたユーザー エクスペリエンスを実現できます。

今回の投稿の要点をまとめると、以下のようになります。

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

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

リソース

Mohammad Rahmani 氏 による Unsplash のヒーロー画像。