ブラウザのプリロード スキャナの概要、パフォーマンス向上の仕組み、対策についてご紹介します。
ページ速度の最適化において見落とされがちな側面の一つに、ブラウザ内部の知識があります。ブラウザは、デベロッパーにはできない方法でパフォーマンスを改善するために特定の最適化を行います。ただし、その最適化が意図せず妨げられない限りは例外です。
内部でブラウザを最適化する方法の 1 つに、ブラウザのプリロード スキャナがあります。この投稿では、プリロード スキャナの仕組みと、さらに重要な点として、プリロード スキャナの妨げを回避する方法について説明します。
プリロード スキャナとは
どのブラウザにも、未加工のマークアップをトークン化してオブジェクト モデルに変換するメインの HTML パーサーがあります。この処理は、<link>
要素で読み込まれたスタイルシートや、async
または defer
属性のない <script>
要素で読み込まれたスクリプトなど、ブロッキング リソースを検出して一時停止するまで続きます。
CSS ファイルの場合は、FOUC(スタイルなしコンテンツのフラッシュ)(スタイル適用前のページのスタイル設定されていないバージョンが短時間表示されること)を防ぐために、解析とレンダリングの両方がブロックされます。
また、defer
属性または async
属性のない <script>
要素が検出された場合、ブラウザはページの解析とレンダリングをブロックします。
プライマリ HTML パーサーの処理中に特定のスクリプトが DOM を変更するかどうか、ブラウザは確実に認識できないためです。このため、JavaScript をドキュメントの最後に読み込むことが一般的であり、解析とレンダリングがブロックされた場合の影響は最小限に抑えられます。
以上が、ブラウザが解析とレンダリングの両方をブロックする正当な理由となります。しかし、このような重要なステップのいずれかをブロックすることは、他の重要なリソースの発見が遅れて番組を視聴できなくなる可能性があるため、望ましくありません。幸いなことに、ブラウザはプリロード スキャナと呼ばれる二次 HTML パーサーによって、こうした問題を軽減するために最善を尽くします。
プリロード スキャナの役割は投機的です。つまり、プリロード スキャナは未加工のマークアップを調べて、プライマリ HTML パーサーがリソースを検出する前に、便宜的にフェッチするリソースを見つけます。
プリロード スキャナの動作を確認する方法
プリロード スキャナは、レンダリングと解析がブロックされたために存在します。この 2 つのパフォーマンスの問題が存在しなければ、プリロード スキャナはあまり役に立たないでしょう。ウェブページがプリロード スキャナの恩恵を受けるかどうかを判断する鍵は、これらのブロック現象によって決まります。そのためには、プリロード スキャナが動作している場所を確認するリクエストに人為的な遅延を発生させます。
例として、スタイルシートを含む基本的なテキストと画像のこちらのページをご覧ください。CSS ファイルはレンダリングと解析のどちらもブロックするため、プロキシ サービスを介したスタイルシートの表示に 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>
要素の後に配置されていると仮定した場合、最適とは言えません。
何が起きたのかを詳しく見てみましょう。
- 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
スクリプトをプリロードする直前のデモでは、スタイルシートは引き続き「最高」優先度で読み込まれますが、スクリプトの優先度は「High」に昇格されています。
リソースの優先度を上げると、ブラウザはより多くの帯域幅をリソースに割り当てます。つまり、スタイルシートの優先度が最も高い場合でも、スクリプトの優先度が高くなると帯域幅の競合が発生する可能性があります。接続速度が遅い場合や、リソースが非常に大きい場合に、これが原因である可能性があります。
ここでの答えは簡単です。起動時にスクリプトが必要な場合、プリロード スキャナを DOM に挿入して無効にしないでください。必要に応じて、<script>
要素の配置や、defer
や async
などの属性でテストしてください。
JavaScript による遅延読み込み
遅延読み込みは、データを保存する優れた方法であり、画像にしばしば適用されます。しかし、「スクロールせずに見える範囲」にある画像に遅延読み込みが誤って適用されることがあります。
これにより、プリロード スキャナが関係するリソースの検出可能性に関する潜在的な問題が生じ、画像への参照の検出、ダウンロード、デコード、表示にかかる時間が不必要に遅延する可能性があります。この画像マークアップの例を見てみましょう。
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
JavaScript の遅延ローダーでは、data-
接頭辞を使用するのが一般的なパターンです。画像がビューポートまでスクロールされると、遅延ローダーは 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>
要素によって読み込まれた別のファイルにあります。このページでは 4 つのウェブフォントも使用しています。これらのウェブフォントは、CSS リソースとは別のファイルとしてリクエストされます。
CSS とすべてのフォントを base64 リソースとしてインライン化するとどうなるでしょうか。
インライン化の影響は、この例の 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 内に含まれ、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
を使用する場合は、ラボツールでテストして、意図したとおりの効果が得られることを確認してください。最後に、大量のリソースをプリロードしすぎないようにしてください。すべてのリソースに優先順位を付けると、何も設定されなくなるからです。
リソース
- スクリプト挿入の「非同期スクリプト」は有害と見なされる
- ブラウザ プリローダによるページの読み込み高速化
- 重要なアセットをプリロードして読み込み速度を改善する
- ネットワーク接続を早期に確立して、認識されるページ速度を改善する
- Largest Contentful Paint の最適化
Mohammad Rahmani 氏 による Unsplash のヒーロー画像。