ブラウザのプリロード スキャナの概要、パフォーマンスへの効果、スキャナを無効にする方法について説明します。
ページ速度の最適化で見落としがちな点の 1 つは、ブラウザの内部についてある程度理解することです。ブラウザでは、デベロッパーが不可能な方法でパフォーマンスを改善するために特定の最適化を行いますが、それは、その最適化が意図せず阻止されない限りです。
内部ブラウザの最適化として、ブラウザのプリロード スキャナがあります。この記事では、プリロード スキャナの仕組みと、スキャナの妨げにならないようにする方法について説明します。
プリロード スキャナとは
すべてのブラウザには、元のマークアップをトークン化してオブジェクトモデルに変換するメインの HTML パーサーがあります。<link>
要素で読み込まれたスタイルシートや、async
属性または defer
属性のない <script>
要素で読み込まれたスクリプトなど、ブロック リソースが検出されるまで、この処理は順調に進みます。
CSS ファイルの場合、スタイルが適用されていないコンテンツのフラッシュ(FOUC)を防ぐためにレンダリングがブロックされます。これは、スタイルが適用される前に、スタイルが適用されていないバージョンのページが一瞬表示される現象です。
また、defer
属性または async
属性のない <script>
要素が検出されると、ページの解析とレンダリングがブロックされます。
これは、メインの HTML パーサーが機能している間に、特定のスクリプトが DOM を変更するかどうかをブラウザが確実に把握できないためです。そのため、ドキュメントの最後に JavaScript を読み込んで、解析とレンダリングがブロックされた場合の影響を最小限にするのが一般的です。
これが、ブラウザで解析とレンダリングの両方をブロックすることが推奨されている正当な理由です。ただし、これらの重要なステップのいずれかをブロックすることは望ましくありません。他の重要なリソースの検出が遅れて、番組の進行が遅れる可能性があります。幸い、ブラウザはプリロード スキャナと呼ばれるセカンダリ HTML パーサーを使用して、これらの問題を軽減するよう最善を尽くしています。
プリロード スキャナの役割は推測的です。つまり、元のマーカーアップを調べて、プライマリ HTML パーサーが検出する前に、その場でフェッチするリソースを見つけます。
プリロード スキャナが動作しているかどうかを確認する方法
プリロード スキャナは、レンダリングと解析がブロックされているため存在します。これらの 2 つのパフォーマンスの問題が存在しなかった場合、プリロード スキャナはあまり役に立ちません。ウェブページがプリロード スキャナの恩恵を受けるかどうかを判断する鍵は、これらのブロック現象にあります。そのためには、リクエストに人為的な遅延を導入して、プリロード スキャナが動作している場所を確認します。
スタイルシートを使用した基本的なテキストと画像のこのページを例に挙げましょう。CSS ファイルはレンダリングと解析の両方をブロックするため、プロキシ サービスを通じてスタイルシートで人為的な遅延(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>
要素の後に配置されていると想定すると、最適な結果は得られません。
ここで何が起こったのかを詳しく見ていきましょう。
- 0 秒でメイン ドキュメントがリクエストされます。
- 1.4 秒で、ナビゲーション リクエストの最初のバイトが届きます。
- 2.0 秒で、CSS と画像がリクエストされます。
- パーサーによるスタイルシートの読み込みがブロックされ、
async
スクリプトを挿入するインライン JavaScript はそのスタイルシートの 2.6 秒後に来るため、スクリプトが提供する機能はすぐには利用できません。
これは、スクリプトのリクエストがスタイルシートのダウンロードが完了した後にのみ行われるため、最適ではありません。これにより、スクリプトができるだけ早く実行されなくなります。一方、<img>
要素はサーバー提供のマーカーアップで検出できるため、プリロード スキャナによって検出されます。
スクリプトを DOM に挿入するのではなく、async
属性を持つ通常の <script>
タグを使用するとどうなりますか?
<script src="/yall.min.js" async></script>
結果は次のようになります。
rel=preload
を使用すると、これらの問題を解決できると思われたくなるかもしれませんが、これは確かに機能しますが、副作用が生じる可能性があります。結局のところ、<script>
要素を DOM に挿入しないことで回避できる問題を rel=preload
を使用して修正する理由はありません。
この場合は、プリロードで問題が「修正」されますが、新しい問題が発生します。最初の 2 つのデモの async
スクリプトは、<head>
に読み込まれるにもかかわらず、優先度が「低」で読み込まれ、スタイルシートが「最高」の優先度で読み込まれます。async
スクリプトがプリロードされている最後のデモでは、スタイルシートは引き続き「最高」の優先度で読み込まれますが、スクリプトの優先度は「高」に引き上げられています。
リソースの優先度を上げると、ブラウザはそのリソースに割り当てる帯域幅を増やします。つまり、スタイルシートの優先度が最も高い場合でも、スクリプトの優先度が引き上げられると、帯域幅の競合が発生する可能性があります。これは、接続が遅い場合や、リソースが非常に大きい場合に要因となる可能性があります。
答えは簡単です。起動時にスクリプトが必要である場合は、DOM に挿入してプリロード スキャナを無効にしないでください。<script>
要素の配置や、defer
や async
などの属性を必要に応じて試します。
JavaScript による遅延読み込み
遅延読み込みは、データを節約するのに効果的な方法で、画像によく適用されます。ただし、いわゆる「上部」の画像にレイジー ローディングが誤って適用されることがあります。
これにより、プリロード スキャナが関与するリソースの検出に問題が生じる可能性があり、画像への参照の検出、ダウンロード、デコード、表示に不要な遅延が生じる可能性があります。たとえば、次の画像マークアップについて考えてみましょう。
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
data-
接頭辞を使用するのは、JavaScript をベースとした遅延読み込みで一般的なパターンです。画像がビューポートにスクロールされると、遅延読み込み機能によって data-
接頭辞が削除されます。つまり、上記の例では data-src
が src
になります。この更新により、ブラウザにリソースの取得を求めるメッセージが表示されます。
このパターンは、起動時にビューポートに表示される画像に適用されるまで問題はありません。プリロード スキャナは、src
(または srcset
)属性と同じ方法で data-src
属性を読み取るため、画像参照が早期に検出されない場合があります。さらに悪いことに、画像の読み込みは、遅延読み込み JavaScript のダウンロード、コンパイル、実行が完了する後まで遅延されます。
画像のサイズ(ビューポートのサイズに依存する場合があります)によっては、Largest Contentful Paint(LCP)の候補要素になる可能性があります。プリロード スキャナが、ページのスタイルシートがレンダリングをブロックしているときに、画像リソースを事前に推測的に取得できない場合、LCP が低下します。
この問題を解決するには、画像マークアップを変更します。
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
プリロード スキャナが画像リソースをより迅速に検出して取得するため、起動時にビューポート内にある画像に最適なパターンです。
この単純化した例では、接続が遅い場合の LCP が 100 ミリ秒改善されています。大きな改善とは見えないかもしれませんが、この解決策はマークアップの簡単な修正であり、ほとんどのウェブページがこの例よりも複雑であることを考慮すると、大きな改善となります。つまり、LCP の候補は他の多くのリソースと帯域幅を争う可能性があるため、このような最適化がますます重要になっています。
CSS 背景画像
ブラウザのプリロード スキャナはマークアップをスキャンします。background-image
プロパティによって参照される画像の取得を伴う可能性がある CSS など、他のリソースタイプはスキャンされません。
HTML と同様に、ブラウザは CSS を独自のオブジェクト モデル(CSSOM)に変換します。CSSOM の構築中に外部リソースが検出された場合は、プリロード スキャナではなく、検出時にリソースがリクエストされます。
ページの LCP 候補が CSS background-image
プロパティを持つ要素であるとします。リソースの読み込みは次のようになります。
この場合、プリロード スキャナは不正行為を防ぐというより、不正行為に巻き込まれないようにするものです。それでも、ページ上の 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
ヒントを使用すると、LCP 候補がより早く検出されるため、LCP 時間が短縮されます。このヒントは問題の解決に役立ちますが、画像 LCP 候補を CSS から読み込む必要があるかどうかを評価することをおすすめします。<img>
タグを使用すると、ビューポートに適した画像の読み込みをより細かく制御しながら、プリロード スキャナがその画像を検出できるようにできます。
リソースのインライン化が多すぎる
インライン化とは、リソースを HTML 内に配置する手法です。<style>
要素内のスタイルシート、<script>
要素内のスクリプト、また、base64 エンコードを使用すると、ほぼすべてのリソースのインライン化が可能です。
リソースに対して個別のリクエストが発行されないため、リソースをインライン化すると、ダウンロードするよりも高速に処理できます。ドキュメントに挿入され、すぐに読み込まれます。ただし、大きな欠点もあります。
- HTML をキャッシュに保存していない場合(HTML レスポンスが動的である場合はキャッシュに保存できない場合)は、インライン リソースはキャッシュに保存されません。インライン化されたリソースは再利用できないため、パフォーマンスに影響します。
- HTML をキャッシュに保存できる場合でも、インライン リソースはドキュメント間で共有されません。これにより、オリジン全体でキャッシュに保存して再利用できる外部ファイルと比較して、キャッシュ保存の効率が低下します。
- インライン化をやりすぎると、余分なインライン コンテンツのダウンロードに時間がかかるため、ドキュメントの後半にあるリソースの検出が遅れます。
このページを例に考えてみましょう。特定の条件下では、LCP の候補がページ上部の画像で、CSS が <link>
要素によって読み込まれる別のファイルにある場合、このページでは、CSS リソースとは別のファイルとしてリクエストされる 4 つのウェブフォントも使用しています。
CSS とすべてのフォントが base64 リソースとしてインライン化されている場合はどうなるでしょうか。
この例では、インライン化の影響により、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 がページの読み込み速度に影響することは間違いありません。デベロッパーはインタラクティビティ機能の提供だけでなく、コンテンツそのものの提供にも Gemini に頼る傾向があります。これはある意味でデベロッパー エクスペリエンスの向上につながりますが、デベロッパーにとってのメリットが必ずしもユーザーにとってのメリットになるとは限りません。
プリロード スキャナを回避できるパターンの 1 つは、クライアントサイドの JavaScript を使用したマークアップのレンダリングです。
マークアップ ペイロードがブラウザの JavaScript に含まれ、JavaScript によって完全にレンダリングされる場合、そのマークアップ内のリソースはプリロード スキャナから実質的に見えなくなります。これにより重要なリソースの検出が遅れ、LCP にも確実に影響します。これらの例では、LCP 画像のリクエストは、JavaScript の表示を必要としない同等のサーバー レンダリングに比べると、著しく遅延しています。
これはこの記事の焦点から少し外れますが、クライアントでマークアップをレンダリングすることの影響は、プリロード スキャナを無効にするだけではありません。たとえば、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
を使用する場合は、ラボツールでテストして、目的の効果が得られることをご確認ください。最後に、リソースを過度にプリロードしないでください。すべてを優先すると、優先するものが何もなくなります。
リソース
- スクリプト挿入の「非同期スクリプト」は問題があると見なされる
- ブラウザのプリローダがページの読み込みを高速化する仕組み
- 重要なアセットをプリロードして読み込み速度を改善する
- ネットワーク接続を早めに確立して、ページの読み込み速度を改善する
- Largest Contentful Paint の最適化
Unsplash のヒーロー画像(Mohammad Rahmani 撮影)