「ファイルの読み込み」とは
ブラウザレンダリングの仕組みを解説するサイトや書籍には、「ファイルを読み込んで〜」のような説明が多くあります。
自分がレンダリング工程を勉強しているときに、この「読み込み」という言葉がファイルのDownload(転送)を指すのか、ファイルのParse(解析)を指すのか、はたまたレンダリング全体のことを言っているのか、説明する場面によって意味が変わる曖昧な言葉だなーと感じていました。
ここではブラウザレンダリングの仕組みについて、1.HTMLのみ、2.HTMLとCSS、3.HTMLとJavaScript、4.HTMLとCSSとJavaScriptの4パターンに分けて、レンダリングフローに定義された言葉に当てはめながら説明していきたいと思います。
(検証環境:Google Chrome バージョン: 80.0.3987.87)
ブラウザレンダリングの仕組みの大枠
ブラウザレンダリングのフローは大きく4つの工程に分けられ、それぞれの工程は更にいくつかの細かい工程に分けられます。
(参考:Webフロントエンド ハイパフォーマンス チューニング -久保田 光則 (著) )
-
Loading(データのダウンロード・解析)
- Download
- Parse
- Scripting(JSの実行)
-
Rendering(スタイルの計算、当て込み)
- Calculate Style
- Layout
-
Painting(描画)
- Paint
- Rasterize
- Composite Layers
図を見ていると全ての工程がシリアル(直列)に進んでいくように誤解しやすいのですが、実際はそうではありません。
レンダリングエンジンがページ表示を最適化する中で、部分的にでも準備ができた段階で、都度次の工程に進むこともあります。
本記事では主にLoading(Download、Parse)とScriptingの工程に関して、ファイルごとにどのように影響を及ぼし合い、レンダリングの処理順が決まっているかについて説明します。
RenderingやPaintingの工程を含むブラウザレンダリング全体の仕組みについては以下記事が詳しいです。
フロントエンジニアなら知っておきたいブラウザレンダリングの仕組みをわかりやすく解説! | LeapIn
1.HTMLのみ
はじめに外部ファイル「読み込み」記述が一切ない純粋なHTMLファイルについて、
ブラウザ検索バーにURLを入力し、HTTPプロトコルで通信してページを表示する場合を考えます。
(参考:ネットワークやTCP/IPやHTTPの基本(初学者向け) - Qiita)
レンダリングの工程としては、まずHTMLのDownloadが始まりますが、
ここでのポイントは、サーバからHTMLファイルなどのリソースが転送される手法は0か1の転送ではなく、
セグメントに分割しながら転送されるということです。
(どのくらいまとめて送るのかについてはサーバサイドで制御するようです)
前提として、ブラウザはUX向上のため画面に何も表示されていない時間を短くするように動きます。
よって全てのHTMLDownloadが完了していなくても、転送されたHTMLセグメントを元に**Parse(DOMツリー構築)**や後続の処理が進み、準備ができたDOMから画面描画が始まります。
上記はChrome DevToolsのNetworkパネルであり、一つのHTMLファイルをダウンロード完了するまでの解析図です。(テスト用にサーバサイド(PHP)でファイルの転送や解析速度を調整しています)
**Waiting(TTFB:Time To First Byte)**とはファイル転送リクエストを送ってからクライアント側で最初のデータを受け取るまでにかかる時間(主にサーバサイドの処理時間)であり、Content Downloadとは最初のデータを受け取ってから全てのデータを受け取りきるまでにかかる時間です。
解析グラフによるとContent Downloadに合計2sかかっていますが、その間も転送されてきているデータを元に別の処理(Parse、Rendering、Painting)が都度進んで描画が始まっており、それは同Performanceパネルで解析することができます。↓
データを受け取る(Receive Data)たびに、**HTMLParse(DOM構築)**のフェーズを経て、Composite Layersまでの描画工程を完了していることが分かります。
このように準備ができたところから都度描画が行われることで、First Paint(画面に最初になにかしらが描画されタイミング)や、First Meaningful Paint(画面に最初にユーザーに意味のある表示がされたタイミング)などの表示タイミング差が存在します。
参考:Ace the Lighthouse Audit: Best Practices for Consistent Interactivity | Lumavate
2.HTMLとCSS
head要素
の中のlink要素
に外部CSS「読み込み」記述がある場合を考えます。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- bodyの中身 -->
</body>
</html>
CSSのDownload
この場合も、まずHTMLのDownload、Parseが始まり、解析途中でlink要素
を見つけた段階でCSSのDownloadが始まります。↓
CSSのDownloadはHTMLのParseをブロックしないので、CSSDownload中もHTMLParseが並行して進みます。
そしてその先に再び外部CSS「読み込み」記述ががあれば、同時に複数のCSSDownloadが始まります。
ただし、モダンブラウザでは(同じドメインの)TCP接続は同時に6本までという制限があるため、7本目以降の接続は前の接続の終了を待ってからとなります。
見ての通りこれではダウンロードしたいファイルが多いほどページ表示速度が遅くなってしまいます。
そのため、対応策としてファイルを可能な限りまとめてリクエスト必要数を抑えたり、CDNなどを利用してあえて別ドメインから接続することでスループットを上げたり、一つのTCP接続で同時に複数のリクエスト/レスポンスを処理できるhttp/2プロトコルで通信するなどの手法が存在します。
参考:そろそろ知っておきたいHTTP/2の話 - Qiita
CSSのParse
CSSもHTMLと同様にDownloadの次の工程として、**Parse(CSSOMの構築)**の工程があります。
考慮すべき注意点は以下です。
- CSSParseは見かけ上はHTMLParseと並行して行われる。
- HTMLは描画工程に進もうとするDOMの、直前までに記載されているCSSの**Loading(Download、Parse)**が完了しない限り、Renderingフローに進まない。(描画処理が行われない)
HTMLParseとCSSParseはどちらもレンダリングエンジンのmainスレッドで行われますが、mainスレッドでは同時に一つの処理しか行えないため、それぞれの処理が同時に走ることはありません。
ですが、HTMLParseのアイドル時間などにCSSParseが進むため、見かけ上は2つが並行して行われているように見えます。
(そもそもCSSParseにかかる時間はブラウザレンダリング全体の時間からすると極めて短く、議論に上がりにくい部分のようです。)
また、CSSのLoadingが進行中の場合は、たとえHTMLParseが先に完了していてもRenderingなどの次の工程に進まず、結果として画面描画が行われません。
これはブラウザがFOUC(Flash of Unstyled Contentの略。スタイルがついていないコンテンツが一瞬表示されること)を防ぐために、CSSParseの完了を待ってスタイルが適応された画面描画を行おうとするためです。
上記Performanceパネル解析図を見ても、Finish Loading(CSSParseの完了)まで、Calculate StyleなどのRendering工程に進んでいない(画面描画が行われていない)ことが分かります。
3.HTMLとJavaScript
以下のようにhead要素
の中にscript要素
を記述して、外部JavaScriptファイルを「読み込む」場合を考えます。
<!DOCTYPE html>
<html>
<head>
<script src="main.js"></script>
</head>
<body>
<!-- bodyの中身 -->
</body>
</html>
JSのDownloadとScripting
HTMLParseが始まってscript要素
に到達するとJSのDownloadが始まります。
その時に重要なポイントが、JSのDownloadとScripting(実行)はHTMLParseをブロックするということです。
一度JSのDownloadが始まると、ダウンロードしたJSのScripting工程が完了しない限り、それ以降のHTMLParseが行われません。
これが、JSの記述はbodyの最後に記述するべきと言われる理由の一つです。
上記図より、Send RequestでJSDownloadが始まると、Evaluate Script工程が完了するまでHTMLParseが行われていないことが分かります。
async属性とdefer属性
script要素
によるJSの「読み込み」記述はそれ以降のHTMLParseをブロックしますが、script要素
にasync
やdefer
の属性をつけることによってJSのDownloadを非同期に行い、HTMLParseと同時に処理することができます。
<script src="main.js" async ></script>
<!-- もしくは -->
<script src="main.js" defer ></script>
以下は先程と同じ記述で、defer属性
を使用したときのPerformanceパネルの解析結果です。
JSのDownloadが開始(send Request)しても、HTMLParseがブロックされずに先の工程に進み、最終的にComposite Layersまで完了して画面描画が行われているのが分かります。
その後JSのDownloadが完了した段階で、**Scripting(Evaluate Script)**処理が行われています。
参考:scriptタグに async / defer を付けた場合のタイミング - Qiita
4.HTMLとCSSとJavaScript
CSSとJavaScriptの両方の「読み込み」記述を書く場合です。
以下のようにlink要素
の直下にscript要素
を入れてみます。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
<script src="main.js"></script>
</head>
<body>
<!-- bodyの中身 -->
</body>
</html>
CSSDownloadはHTMLParseをブロックしないため、HTMLParseはscript要素
の記述に到達しJSのDownloadが始まります。
先程「HTMLは直前までのCSSLoading(Download、Parse)が完了していない限り、Renderingフローに進まない」と説明しましたが、実は同様にJSも直前までのCSSLoading(Download、Parse)が完了していない限り、Scriptingの工程に進まない性質があります。
つまりこの場合、CSSよりもJSのほうが速くDownloadが完了したとしても、CSSParseが完了するまでScriptingが待機状態になるということです。
↑JSのほうがCSSよりも1s速くDownloadが完了していますが、
↑CSSの**Loading(Download、Parse)**完了を待ってから、**Scripting(Evaluate Script)**処理が実行されていることが分かります。
参考:DOMContentLoaded周りの処理を詳しく調べてみました - Qiita
ブラウザのプリロード機能
以下のようにJSの「読み込み」記述をCSSよりも前に書いた場合を考えます。
<!DOCTYPE html>
<html>
<head>
<script src="main.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- bodyの中身 -->
</body>
</html>
defer属性
やasync属性
がついていないscript要素
による外部JSファイルの「読み込み」なので、JSのDownload、Scriptingが完了するまでそれ以下のHTMLParseが進まない、つまりCSSDownloadも進まないはずです。
しかし、モダンブラウザではその限りではありません。
NetWorkパネルを見てみると、JSとCSSのDownloadが同時に行われていることが分かります。
実はChromeなどのモダンブラウザには、HTMLParseが進んでいない部分についてもDownloadが必要な記述がないか確認し、もしあれば事前にそのファイルのDownloadを開始する機能があります。(Preload Scanner)
よってこの場合も、ブラウザはJSのDownload中にその先にあるCSSの「読み込み」記述を読み取り、CSSDownloadも同時に進めることでレンダリングを高速化しているのです。
※Preload Scanner機能で事前処理できるのはDownloadの工程だけです。ParseやScriptingの工程は本来のレンダリングフローに沿って行われます。
参考:rel="preload"を極めるために必要な2種類のプリロード機能 | Raccoon Tech Blog
まとめ
- HTMLはセグメントごとにDownloadが行われ、都度Parseなどの先の工程に進む
- CSSのDownloadはHTMLのParseをブロックしない
- CSSのParseは見かけ上はHTMLParseと並行して行われる
- HTMLは直前までの**CSSLoading(Download、Parse)**が完了していない限り、Renderingの工程に進まない
- JSのDownloadとScripting(実行)はHTMLのParseをブロックする
- JSも直前までの**CSSLoading(Download、Parse)**が完了しない限り、Scriptingの工程に進まない
誤った解釈等ございましたら、ご教授お願いいたします。。
参考
- Webフロントエンド ハイパフォーマンス チューニング -久保田 光則 (著)
- フロントエンジニアなら知っておきたいブラウザレンダリングの仕組みをわかりやすく解説! | LeapIn
- ネットワークやTCP/IPやHTTPの基本(初学者向け) - Qiita
- Ace the Lighthouse Audit: Best Practices for Consistent Interactivity | Lumavate
- そろそろ知っておきたいHTTP/2の話 - Qiita
- scriptタグに async / defer を付けた場合のタイミング - Qiita
- DOMContentLoaded周りの処理を詳しく調べてみました - Qiita
- rel="preload"を極めるために必要な2種類のプリロード機能 | Raccoon Tech Blog
- フロントエンドのパフォーマンスを徹底解説!ブラウザの気持ちで理解するHTML/Javascript/CSSの話 | Raccoon Tech Blog