ブラウザの投機的パース処理とリソース優先度
なぜ script を head に置くと表示が止まり、画像に fetchpriority を付けると LCP が縮むのか。プリロードスキャナとリソース内部優先度の原理を、効かせ方まで詳説します。
- 1.HTMLパーサ本体が同期 script で止まっている間も、プリロードスキャナが先のトークンを走査して img・CSS・script などのURLを先行ダウンロードする。これが初期表示の取得を前倒しする中核。
- 2.同期 script はパースをブロックし、後続の script は先行する CSSOM 構築の完了を待つ。だから head の重い script は描画を直列に止める。
- 3.ブラウザは要素種別・位置・属性からリソースに内部優先度を割り当てる。fetchpriority はその値を high/low に上書きするヒントで、特に LCP 画像の前倒しに効く。
投機的パースとは何か:メインパーサと先読みの二段構え
ブラウザがHTMLを受信してDOMを組み立てるパーサは、本質的に直列です。トークンを先頭から順に消費し、<script>(インラインまたは同期外部)に出会うとスクリプトの取得・実行が終わるまでDOM構築を止めます。素朴に実装すると、この停止中はネットワークが完全に遊んでしまい、後続に並ぶCSSや画像のダウンロードが始まりません。
これを救うのがプリロードスキャナ(preload scanner、投機的パーサ)です。メインパーサが詰まっている間、スキャナは入力バッファのまだDOM化していない先の部分を軽量に走査し、src / href を持つトークン(img、link rel=stylesheet、script src、video の poster など)からURLを抜き出して先行的に取得を開始します。DOMツリーは作らず、属性も完全には解釈しない「投機的」な読みなので、メインパーサより速く先へ進めます。
メインパーサ: <head>……<script>―――(取得+実行で停止)―――▶ …<body><img>…
▲ここで停止しても
プリロードスキャナ: ───先のバッファを走査して img/css/js のURLを先に発見し取得開始───▶
要点は、取得(フェッチ)の前倒しであってDOM構築の前倒しではないことです。スキャナは「何を取りに行くべきか」を早く知るための仕組みであり、実行順序やDOMの意味づけは依然メインパーサが担います。基礎の全体像は ブラウザのレンダリングの仕組み を参照してください。
パーサブロッキング:script と CSS が描画を止める条件
なぜ <script> の位置が決定的なのか。鍵はスクリプトとスタイルの相互依存にあります。
- 同期 script はパースをブロックする。
<script src>(async/deferなし)はDOM構築を止め、取得と実行を完了させてから次へ進みます。これはスクリプトがdocument.writeで後続のトークン列を書き換えうるため、ブラウザが先のDOMを確定できないからです。 - script は先行する CSS の完了を待つ。 スクリプトは
getComputedStyleなどでスタイルを読み取れるため、ブラウザは未完成のCSSOMでスクリプトを走らせるわけにいきません。結果として、ある同期スクリプトの実行は、それより前に現れた<link rel=stylesheet>のCSSOM構築完了を待ちます。
head に <link rel=stylesheet> と同期 <script> がこの順で並ぶと、(1) CSS取得→CSSOM構築、(2) その完了を待ってスクリプト実行、(3) ようやくパース再開、と直列になります。スクリプト自体は描画をブロックしますが、その手前でCSSの完了待ちまで挟まるため、体感はさらに悪化します。async / defer を付けるとパースブロッキングが外れます。
| 指定 | 取得タイミング | 実行タイミング | パースブロック |
|---|---|---|---|
| script(無印・外部) | 発見後すぐ | 取得完了後すぐ(DOM到達順) | する |
| script async | 発見後すぐ | 取得完了次第(順不同) | しない |
| script defer | 発見後すぐ | パース完了後・DOMContentLoaded前(記述順) | しない |
| link rel=stylesheet | 発見後すぐ | — | 描画をブロック(後続scriptも待たせる) |
CSS はレンダリングブロッキングリソースです。スタイルが未確定のまま描画すると、いわゆるスタイルなしコンテンツのちらつき(FOUC)が起きるため、ブラウザは重要なCSSのCSSOM構築が終わるまで最初のペイントを抑制します。async / defer でも、document.write を使わないスクリプトでも、このCSSの描画ブロックは別の話として残ります。
リソースの内部優先度:ブラウザが自動で付ける順位
取得すべきURLが分かっても、回線とサーバーの同時接続には限りがあります。そこでブラウザは各リクエストに内部優先度(resource priority)を割り当て、スケジューラがこの優先度に従って発行順とネットワーク上の重みを決めます。HTTP/2 以降では、これがストリームの重み・依存としてサーバーへ伝わります(詳細は HTTP/2の多重化とHPACKヘッダ圧縮の原理 を参照)。
優先度はおおむね次の観点から自動決定されます(Chromium の挙動を基準とした概略で、ブラウザ差があります)。
| リソース | 既定の優先度(概略) | 決め手 |
|---|---|---|
| HTML(ドキュメント) | Highest | 後続すべての起点だから |
| CSS(head のstylesheet) | Highest | 描画をブロックするため最優先 |
| 同期 script(早い位置) | High | パースを止めるため |
| script async / defer | Low | 描画を妨げないため |
| フォント | High | テキスト描画に直結 |
| ビューポート内の画像 | Low→Medium | レイアウト確定後に引き上げ |
| ビューポート外の画像 | Lowest | 今は見えないため |
ここで効いてくるのが位置とビューポートです。画像はマークアップ発見時点では既定で低優先ですが、レイアウトが進んで初期ビューポート内にあると判明すると、ブラウザは優先度を引き上げ直すことがあります。逆に画面外の画像は低いまま据え置かれます。<head> 内のCSSやフォントが高いのは、それらが描画を直接律速するからです。
プリロードスキャナは「いつ発見するか(前倒し)」、内部優先度は「発見したものをどの順で流すか(重み付け)」を担います。スキャナが早く画像を見つけても、その画像が低優先なら、高優先のCSSやフォントの後ろに回されます。両者を分けて理解すると、後述の fetchpriority がどこに効くかが明確になります。
Priority Hints(fetchpriority)の効き方と限界
fetchpriority 属性(high / low / auto)は、ブラウザが自動算出した内部優先度を上書きするヒントです。順序を直接固定するのではなく、スケジューラへの優先度シグナルを変える点が肝心です。
<!-- LCP となるヒーロー画像を最優先で取りに行かせる -->
<img src="hero.webp" fetchpriority="high" width="1200" height="630" alt="主役の写真" />
<!-- 重要度の低い装飾画像やサードパーティ画像を後ろへ下げる -->
<img src="decoration.webp" fetchpriority="low" alt="" />
<!-- preload と併用して、本当に最初に要るものを明示 -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high" />
最も費用対効果が高い使い所はLCP画像です。前述のとおり画像の既定優先度は低く、ブラウザがビューポート内と判定して引き上げるまでに一拍遅れが出ます。fetchpriority="high" を主役画像に付けると、その判定を待たず初手から高優先で取得を始められ、LCP が縮みます。逆に、ファーストビューに不要なカルーセルの2枚目以降などを low に下げると、重要リソースへ帯域を譲れます。
<link rel="preload"> は「プリロードスキャナより前に、確実に発見させる」ための先回り宣言です。一方 fetchpriority は「発見済みリソースの優先度を変える」ヒントです。CSS の background-image や JS で動的生成する画像など、スキャナが見つけられないリソースは preload で発見を早め、そこへ fetchpriority="high" を重ねると効果が最大化します。
fetchpriority="high" を多くの要素に付けると、優先度の差が消えて全体が遅くなります。優先度は相対的な順位付けなので、「全部 high」は「全部 normal」と同義です。また fetchpriority はあくまでヒントで、<head> のCSSやフォントといった本来高優先のリソースを押しのけて画像を最優先化することは保証されません。レンダリングブロッキングの解消(async / defer、クリティカルCSSの分離)が先で、fetchpriority はその上の微調整です。
実務での適用指針
出題・実務で問われるのは、(1) プリロードスキャナが「取得の前倒し」であってDOM構築の前倒しではない点、(2) 同期 script がパースをブロックし、かつ先行CSSのCSSOM完了を待つ理由(document.write とスタイル読み取り)、(3) 内部優先度が要素種別・位置・ビューポートで自動決定される点、(4) fetchpriority がその優先度を上書きする「ヒント」であり、相対順位なので濫用すると無効化する点、の4つです。
- クリティカルパスを直列にしない:head の同期 script を避け、
defer(記述順を保つ)かasync(順不同で良いもの)へ。CSS は必要分を小さく保ち、描画ブロック時間を短くする。 - LCP画像を前倒し:主役画像に
fetchpriority="high"を付け、必要ならpreloadで発見も早める。ファーストビュー画像にloading="lazy"は付けない。 - スキャナの死角を埋める:CSS背景やJS生成のリソースはスキャナに見えないため、
preloadで明示する。 - 優先度は相対:
highは本当に効かせたい1〜2個に絞り、不要なものをlowに下げて差を作る。
まとめ
ブラウザはメインパーサでDOMを直列に組みつつ、プリロードスキャナで先のトークンを走査して画像・CSS・JSの取得を前倒しします。同期 script はパースをブロックし、先行CSSのCSSOM完了まで待つため、head の重い script は描画を直列に止めます。取得すべきURLが決まると、ブラウザは要素種別・位置・ビューポートから内部優先度を自動付与し、fetchpriority はその値を上書きするヒントとして働きます。特に低優先になりがちなLCP画像へ high を与えると表示が縮みます。仕上げに レンダリングパイプライン詳説 と Web パフォーマンス を合わせると、取得から描画までの律速点を一本の線で説明できます。
Web/フロントエンド Article
ブラウザの投機的パース処理とリソース優先度を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ブラウザ
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
同期 script はパースをブロックし、後続の script は先行する CSSOM 構築の完了を待つ。だから head の重い script は描画を直列に止める。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「ブラウザ / パフォーマンス」に近いか確認する。
- 強みである「HTMLパーサ本体が同期 script で止まっている間も、プリロードスキャナが先のトークンを走査して img・CSS・script などのURLを先行ダウンロードする。これが初期表示の取得を前倒しする中核。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。