ノベルティメディア
mediaブラウザレンダリングを徹底解剖【Parse/Scripting/Style編】
動画クリエーター兼コーダーの川島です。
この記事では、以前執筆したブラウザレンダリングの処理フローにおけるParse、Scripting、Style処理の詳細を知ることができます。
ブラウザレンダリングの概要については過去記事の『ブラウザレンダリングの仕組みを理解し、Webサイトのパフォーマンス向上を目指す』を参照ください。
Loadingフェイズで行われていること
上図のように、Loadingフェイズではふたつの処理が行われています。
「Download」処理と「Parse」処理です。
Downloadフェイズとは、サーバーからデータをダウンロードしてくるフェイズ(Downloadフェイズの詳しい内容に関しては、過去記事『ブラウザレンダリングを徹底解剖【Download編】』を参照ください)、
Parseフェイズとは、Downlodフェイズの次に訪れるフェイズで、コードを解析(Parse)し、DOMツリーと CSSOMツリーへ変換するフェイズです。
また、図上ではLoadingフェイズとは乖離して表現されていますが、その後に続くScripting/Styleフェイズも、Parseフェイズに密接に関係しています。
今回は、ブラウザレンダリングにとって2/8フェイズにあたるParseフェイズを軸に、Scripting、Styleフェイズまでを詳しく見ていきます。
Parseの処理フロー
下図は、『ブラウザレンダリングの仕組みを理解し、Webサイトのパフォーマンス向上を目指す』の記事内でもご紹介したブラウザレンダリングフローの図解画像です。この図の、「Render Trree構築」までがParse処理の流れを示していることになります。
1. HTMLドキュメントの読み込み
ブラウザはサーバーからHTMLドキュメントをダウンロードし、テキストとして読み込み、解析を始めます。
2. CSSドキュメントの読み込み
HTMLドキュメント内で参照される外部CSSファイルや、<style>タグ内のスタイルを読み込みます。
HTMLとCSSのパース処理はほぼ同時に行われますが、厳密にはHTMLドキュメントが先に読み込まれ、その中に記載してある<style>や<link>を検知し次第、CSSドキュメントの読み込みと解析が開始されます。
3. DOM/CSSOMツリーの構築
それぞれのドキュメントが読み込まれると、次にブラウザはDOM/CSSOMツリーの構築を開始します。
3-1.トークン化プロセス(HTML)
ウェブブラウザがHTMLドキュメントを読み込むとき、最初に行うのがトークン化です。例えば、
<p class="sample">サンプルテキスト</p>
上記のようなHTMLのテキストデータは、以下のようなトークンに分割されます。
- 開始タグトークン:<p>
- 終了タグトークン: </p>
- テキストトークン:サンプルテキスト(タグの間にあるテキスト)
- 属性トークン:class="sample"(タグ内の属性)
このプロセスを通じて、ブラウザはHTMLドキュメントの構造を理解し、DOM(Document Object Model)ツリーを構築するための情報を得ます。
3-2.トークン化プロセス(CSS)
CSSもまた、ブラウザによってトークン化されます。CSSのテキストデータは、セレクタ、プロパティ、プロパティの値などに分割されます。例えば、
body { color: red; }
上記のようなHTMLのテキストデータは、以下のようなトークンに分割されます。
- セレクタトークン:body
- プロパティ名トークン: color
- プロパティ値トークン:red
- ブロック開始トークン:{
- ブロック終了トークン:}
CSSのトークン化によって、CSSOM(Document Object Model)ツリーを構築するための情報を得ます。
3-3.トークンをノードへ変換
トークン化プロセスによって生成されたトークンは、それぞれ対応するDOMノードに変換されます。例えば、<p>の開始タグトークンはp要素のDOMノードに、テキストトークンはテキストノードに変換されます。
先ほどのHTMLドキュメントを例にとると、
- 要素ノード:<p>
- テキストノード:サンプルテキスト
- 属性ノード:class="example"
となります。トークンとして一度分解された要素は、ノードという形で再び構造化された形に変換されます。
3-4.DOM/CSSOMツリーの構築
変換されたノードを使用し、ようやくDOM/CSSOMツリーの構築が行われることとなります。
親子関係を持つDOMツリーに対し、CSSOMツリーはDOMツリーの各ノードに適用されるスタイル情報を階層的に管理します。
3-5.スクリプトの処理
<script>タグで指定されたJavaScriptは、同期的に読み込まれるため、ブラウザによってその場で読み込みと実行が行われます。これにより、スクリプトが完全に読み込まれ実行されるまで、後続のHTML/CSSのパース処理がブロックされます(Blocking)。
初期表示に必要のないスクリプトは非同期や遅延させて読み込むなど、なるべくパース処理中にブロッキングを起こさせないように努める必要があります。
4. Renderツリーの構築
DOMツリーとCSSOMツリーが完成すると、ブラウザはこれら二つのツリーを統合、画面上に表示される要素のみを抽出し、Renderツリーを構築します。
Renderツリーは、実際に画面に表示される要素とそのスタイル情報を含みます。
この過程で、display: none;などのスタイルが適用されている要素はレンダーツリーに含まれないことになりますので、DOMツリーとRenderツリーは必ずしも一致しないことになります。
Scripting/Styleフェイズについて
Parseフェイズの処理フローを見てわかるように、Scripting、Styleなどの処理については、Parse処理の最中だけでなく、Renderツリー構築後などにも走るフェイズとなりますので、厳密な順序関係を示すことは困難です。
ですので、Scripting/Style処理についてはParse処理中、必要に応じ並行して行われているフェイズと理解しましょう。
Parse処理が重くなる原因
さて、ここからはParse処理を重くしてしまう原因を考えてみます。
1.大量のHTML/CSS/JavaScript
ドキュメントサイズが大きい場合、ブラウザがHTML、CSS、JavaScriptを解析し、DOMツリー、CSSOMツリー、およびJavaScriptの実行コンテキストを構築するために多くのリソースが必要となります。特に、JavaScriptの量が多い場合、パースと実行に時間がかかります。
解決策
コードを分割して遅延読み込みを行う、不要なコードを削除する、ミニファイ(圧縮)を行うなどして、ページサイズを削減する。
2.複雑なCSSセレクタ
複雑なCSSセレクタ(特に多層にわたるセレクタや全体セレクタ)は、ブラウザがCSSOMを構築する際にパフォーマンスに影響を及ぼすことがあります。ブラウザは、CSSセレクタのマッチング処理により、どのルールがどの要素に適用されるかを決定する必要があります。
解決策
シンプルで効率的なセレクタを使用し、不要なセレクタのネストを避けることで、パース時間を短縮します。
2-1.複雑なCSSセレクタの例
・深いネスト構造
body header nav ul li a {...}
この例では、<a>をスタイリングするために5レベルのネストが使用されています。ブラウザはこのセレクタを解析する際に、多数の要素をチェックする必要が出てきてしまいます。
・多数のクラスセレクタ
.navbar .menu .item .link.active {...}
複数のクラスが連結されている例です。これにより特定の要素を非常に具体的にターゲットすることができますが、セレクタの解析に時間がかかる可能性があります。
・属性セレクタ
input[type="text"][name$="name"] {...}
属性セレクタを使用すると、特定の属性を持つ要素に対してスタイルを適用できますが、特に属性値の一部にマッチするセレクタを使う場合(この例ではname属性がnameで終わるinputタグ)、ブラウザの処理負担が増えます。
・疑似クラスと疑似要素の組み合わせ
a:hover > .dropdown::before {...}
疑似クラスと疑似要素の組み合わせも、ブラウザにとっては解析が複雑になり得ます。
・ユニバーサルセレクタとの組み合わせ
* > .button {...}
ユニバーサルセレクタは任意の要素にマッチし、これが他のセレクタと組み合わされると、ブラウザは文書内の多くの要素をチェックする必要があります。
3.JavaScriptの同期的読み込みと実行
Parse処理中にjsの読み込みが発生すると、DOM/CSSOMツリーの構築の一時停止(Blocking)が発生してしまうと、Parse処理にかかる時間が増加してしまいます。
改善策
必要なJavaScriptをページの下部に移動させる、asyncやdefer属性を使用して非同期に読み込むことで、パースの遅延を避ける。
4.外部リソースの読み込み
外部のCSSファイルやJavaScriptファイル、フォントなどのリソースが多い場合、これらのリソースの読み込みが完了するまでParse処理がブロックされることがあります。
解決策
リソースの結合やミニファイ、非同期または遅延読み込みの適用、CDN(コンテンツ配信ネットワーク)の利用などで読み込み時間を短縮します。
5.頻繁なDOMの操作
JavaScriptを使用してDOM要素を繰り返し追加、削除、または変更など行うと、それぞれの操作でリフローやリペイントが発生します。
解決策
DOMの変更をバッチ処理する、仮想DOMを使用する(Reactなどのフレームワークを利用)、または必要最小限のDOM操作に抑える。
6.スタイルの動的変更
JavaScriptで要素のスタイル(位置、サイズ、色など)を動的に頻繁に変更すると、変更の度にリペイントやリフローがトリガーされます。
解決策
CSSクラスを切り替えることでスタイルを変更するなど。
7.サイズや位置を頻繁に計算する
JavaScriptでoffsetWidth, offsetHeight, getBoundingClientRect()など、レイアウト情報を取得するプロパティを頻繁にアクセスすると、ブラウザは正確な情報を提供するためにリフローを実行されてしまいます。
また、レイアウトに影響を与えるCSSプロパティ(例:width, height, margin)をアニメーション化すると、アニメーションのフレームごとにリフローやリペイントが必要になってしまいます。
解決策
レイアウト情報の読み取りを最小限にし、可能な限り一度にまとめて読み取るようにし、transformやopacityのような、リペイントのみをトリガーするCSSプロパティを使ったアニメーションにする。
Parseフェイズの高速化を目指すには
Parseフェイズで行われている処理フローを理解し、ブロッキングをなるべく起こさず、効率的でシンプルなコーディングを心がけ、スムーズなツリー構築を促す意識を持つことが重要と言えます。
おすすめ記事/ PICKUP
記事カテゴリー/ CATEGORY
企業の課題はノベルティひとつで完結
ホームページ制作などのWeb制作をはじめ、
システム開発やマーケティング支援などワンストップで対応
まずはお気軽にお問い合わせください
お電話またはメールでお気軽にお問い合わせください。
各種サービスの資料をご用意しています