ブラウザ プリロード スキャナとは何か、それがどのようにパフォーマンス向上に役立つのか、どうすればバグを回避できるのかを解説します。
ページ速度を最適化するうえで見落とされがちな側面の一つとして、ブラウザの内部構造についての知識が欠かせません。ブラウザでは、デベロッパーが不可能な方法でパフォーマンスを改善するために特定の最適化を行いますが、それは、その最適化が意図せず阻止されない限りです。
理解しておくべき内部ブラウザ最適化の 1 つは、ブラウザのプリロード スキャナです。この投稿では、プリロード スキャナの仕組みに加えて、プリロード スキャナの回避方法について説明します。
プリロード スキャナとは
どのブラウザにも、未加工のマークアップをトークン化してオブジェクト モデルに処理するプライマリ HTML パーサーがあります。これは、<link>
要素で読み込まれたスタイルシートや、async
または defer
属性のない <script>
要素で読み込まれたスクリプトなど、ブロッキング リソースを検出したときにパーサーが一時停止するまで、順調に続きます。
CSS ファイルの場合、スタイルが設定されていないコンテンツ(FOUC)が現れるのを防ぐために、解析とレンダリングの両方がブロックされます。FOUC とは、スタイルが適用されていないバージョンのページに、スタイルが適用される前に短時間表示されることを指します。
<ph type="x-smartling-placeholder">また、defer
または async
属性のない <script>
要素が検出された場合、ブラウザはページの解析とレンダリングをブロックします。
これは、メインの HTML パーサーが機能している間に、特定のスクリプトが DOM を変更するかどうかをブラウザが確実に把握できないためです。そのため、ドキュメントの最後に JavaScript を読み込むことで、解析とレンダリングがブロックされた場合の影響を最小限にとどめるのが一般的でした。
これが、ブラウザで解析とレンダリングの両方をブロックすることが推奨されている正当な理由です。しかし、これらの重要なステップのいずれかをブロックすることは望ましくありません。他の重要なリソースの発見が遅れて番組を進行できなくなる可能性があるためです。幸いなことに、ブラウザは「プリロード スキャナ」という二次的な HTML パーサーを使用して、この問題を最善を尽くします。
<ph type="x-smartling-placeholder">プリロード スキャナの役割は投機的です。つまり、プライマリ HTML パーサーが検出する前に、未加工のマークアップを調べて、状況によってフェッチするリソースを見つけます。
プリロード スキャナが動作していることを確認する方法
プリロード スキャナは、レンダリングと解析がブロックされているために存在します。この 2 つのパフォーマンスの問題がまったく発生していなければ、プリロード スキャナはあまり役に立たないでしょう。ウェブページにプリロード スキャナが役立つかどうかを判断するには、こうしたブロック現象が重要になります。そのためには、プリロード スキャナが動作している場所を検出するためのリクエストを人為的に遅延させます。
基本的なテキストと画像に関するこちらのページを例に、スタイルシートを追加します。CSS ファイルはレンダリングと解析の両方をブロックするため、プロキシ サービスを通じてスタイルシートで人為的な遅延(2 秒)が発生します。この遅延により、ネットワーク ウォーターフォールでプリロード スキャナが動作している場所がわかりやすくなります。
<ph type="x-smartling-placeholder">ウォーターフォールからわかるように、レンダリングとドキュメントの解析がブロックされていても、プリロード スキャナは <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>
要素の後にあると仮定すると、最適な結果が得られません。
ここで何が起こったのかを詳しく見ていきましょう。
- 0 秒後に、メイン ドキュメントがリクエストされます。
- 1.4 秒で、ナビゲーション リクエストの最初のバイトが到着します。
- 2.0 秒で CSS と画像がリクエストされます。
- パーサーによるスタイルシートの読み込みがブロックされており、
async
スクリプトを挿入するインライン JavaScript はそのスタイルシートの 2.6 秒後に来るため、スクリプトが提供する機能はすぐには利用できません。
スクリプトのリクエストはスタイルシートのダウンロードが完了した後にのみ発生するため、最適な方法ではありません。これにより、スクリプトの実行を遅らせることができます。一方、<img>
要素はサーバーが提供するマークアップで検出できるため、プリロード スキャナで検出されます。
それでは、スクリプトを DOM に挿入するのではなく、通常の <script>
タグを async
属性とともに使用するとどうなるでしょうか。
<script src="/yall.min.js" async></script>
結果は次のようになります。
<ph type="x-smartling-placeholder">rel=preload
を使用すると、これらの問題を解決できると思われたくなるかもしれませんが、これは確かに効果がありますが、なんらかの副作用が生じることがあります。結局のところ、<script>
要素を DOM に挿入しないことで回避できる問題を修正するために、rel=preload
を使用する理由は何でしょうか。
「修正」のプリロードここでは問題がありますが、新たな問題が発生します。最初の 2 つのデモの async
スクリプトが(<head>
に読み込まれるにもかかわらず)「Low」で読み込まれるというものです。優先されますが、スタイルシートは「最高」のあります。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
になります。この更新により、ブラウザはリソースを取得するよう促します。
このパターンは、起動時にビューポートに表示される画像に適用されるまで問題はありません。プリロード スキャナは data-src
属性を src
(または srcset
)属性と同じ方法で読み取らないため、画像参照はこれより前には検出されません。さらに悪いことに、遅延ローダの JavaScript がダウンロード、コンパイル、実行を行った後まで画像の読み込みが遅延します。
画像のサイズ(ビューポートのサイズによって異なる)によっては、Largest Contentful Paint(LCP)の候補要素となる可能性があります。ページのスタイルシートがレンダリングをブロックしているときなどに、プリロード スキャナが事前にイメージ リソースを投機的に取得できない場合、LCP が失敗します。
この問題を解決するには、次のように画像のマークアップを変更します。
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
プリロード スキャナはより迅速に画像リソースを検出して取得できるため、これは起動時にビューポートにある画像に最適なパターンです。
<ph type="x-smartling-placeholder">この簡素化された例では、低速の接続での 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>
要素によって読み込まれた別のファイル内にあります。また、このページでは 4 つのウェブフォントが使用されており、これらは CSS リソースから個別のファイルとしてリクエストされます。
CSS とすべてのフォントが base64 リソースとしてインライン化されている場合はどうなるでしょうか。
<ph type="x-smartling-placeholder">インライン化の影響は、この例の LCP だけでなく、一般的なパフォーマンスにも悪影響を及ぼします。何もインライン化していないバージョンのページは、約 3.5 秒で LCP 画像を描画します。すべてをインライン化するページでは、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 を使用したマークアップのレンダリングです。
<ph type="x-smartling-placeholder">マークアップのペイロードが JavaScript によってブラウザ内に含まれ、完全にレンダリングされると、そのマークアップ内のリソースはプリロード スキャナから実質的に見えなくなります。これにより重要なリソースの検出が遅れ、LCP にも確実に影響します。これらの例では、LCP 画像のリクエストは、JavaScript の表示を必要としない同等のサーバー レンダリングに比べると、著しく遅延しています。
これはこの記事の焦点とは少し異なりますが、マークアップのレンダリングがクライアントに及ぼす影響は、プリロード スキャナを無効にするだけにとどまりません。たとえば、JavaScript を導入して必要のないエクスペリエンスを強化すると、不要な処理時間が発生し、Next Paint(INP)に影響する可能性があります。クライアントで大量のマークアップをレンダリングすると、サーバーから送信される同じ量のマークアップと比べて、時間のかかるタスクが生成される可能性があります。その理由は、JavaScript に付随する余分な処理を除けば、ブラウザがサーバーからマークアップをストリーミングし、長いタスクを制限しがちな方法でレンダリングを分割するためです。一方、クライアントがレンダリングするマークアップは、単一のモノリシック タスクとして処理されるため、ページの INP に影響する可能性があります。
このシナリオに対する対処方法は、「ページのマークアップを、クライアント上でレンダリングされるのではなく、サーバーから提供できないのはなぜか」という質問に対する答えによって異なります。答えが「いいえ」の場合、可能な限りサーバーサイド レンダリング(SSR)や静的に生成されたマークアップを検討する必要があります。そうすることで、プリロード スキャナが重要なリソースを前もって検出し、機会に応じて取得しやすくなります。
ページのマークアップの一部に機能を付加するために JavaScript が必要な場合でも、SSR(Vanilla JavaScript またはハイドレーション)を使用して両方の利点を活かすことができます。
プリロード スキャナが
プリロード スキャナは、起動時にページの読み込み時間を短縮する非常に効果的なブラウザ最適化です。重要なリソースを前もって検出できなくなるパターンを避けることで、開発が簡単になるだけでなく、ウェブに関する指標などの多くの指標でより良い結果を生み出すユーザー エクスペリエンスが向上します。
以下に、この投稿の重要なポイントをまとめます。
- ブラウザのプリロード スキャナは二次的な HTML パーサーです。より早く取得できるリソースを、不都合に検出するためにプライマリ パーサーの前にスキャンします。
- 最初のナビゲーション リクエストでサーバーによって提供されるマークアップに存在しないリソースは、プリロード スキャナで検出できません。プリロード スキャナを無効にする方法には、次のようなものがあります(これらに限定されません)。
<ph type="x-smartling-placeholder">
- </ph>
- JavaScript を使用してリソースを DOM に挿入します。スクリプト、画像、スタイルシートなど、サーバーからの最初のマークアップ ペイロードに含める方がよいリソースであれば何でもかまいません。
- JavaScript ソリューションを使用して、スクロールせずに見える範囲の画像や iframe を遅延読み込みする。
- JavaScript を使用して、ドキュメント サブリソースへの参照を含む可能性のあるマークアップをクライアントでレンダリングする。
- プリロード スキャナは HTML のみをスキャンします。LCP の候補など、重要なアセットへの参照が含まれている可能性のある他のリソース(特に CSS)のコンテンツは調査しません。
なんらかの理由で、読み込みパフォーマンスを高速化するプリロード スキャナの機能に悪影響を及ぼすパターンを回避できない場合は、rel=preload
リソースヒントを検討してください。rel=preload
を使用する場合は、ラボツールでテストを行い、意図した効果が得られることを確認してください。最後に、プリロードするリソースの数が多くなりすぎないようにしましょう。優先度を高く設定しても、何も優先度が上がらないからです。
リソース
- スクリプト挿入の「非同期スクリプト」有害とみなされる
- ブラウザのプリローダーによってページの読み込みが高速化される仕組み
- 重要なアセットをプリロードして読み込み速度を改善する
- ネットワークの接続を早期に確立して、認識されるページ速度を改善する
- Largest Contentful Paint の最適化
Unsplash のヒーロー画像(著者: Mohammad Rahmani) 。