🤖

ブラウザの仕組みについて学んだのでまとめます

2023/05/29に公開

記事を書くきっかけ

先日、次のドキュメントを読んでブラウザの仕組みを学びました。

これらのドキュメントから多くの学びを得られましたが、私にとっては情報が詰まっているため、整理して知識を確認するためにこの記事を作成しました。個人的に特に学びになったと思ったところを中心にまとめています。ちなみに私は英語が苦手なので主に日本語訳されたドキュメントを読みました。

はじめに

Web開発者にとってブラウザの内部動作を知ることは、よりよい判断をすること・開発のベストプラクティスの背後にある理由を知る手助けになります。

取り上げるブラウザ

上記のドキュメントでは、FireFox、Chrome、Safariを取り上げています。
上記のドキュメントでは2011年8月時点でFirefox、Safari、Chromeのシェアを合計すると60%に達しますとの記載がありましたが、2023年4月では、こちらのサイトを参照したところ約86%となっていました。

ブラウザの主要な機能

ブラウザの主要な機能はユーザーが選択したWebのリソースを表示させることです。

ブラウザの主要なコンポーネント

ブラウザの主要なコンポーネントを次に示します。

  1. ユーザーインターフェース: アドレスバー、戻る・進むボタンなどのメインウィンドウに表示される以外のもの
  2. ブラウザエンジン: UIとレンダリングエンジン間のアクションを制御するもの
  3. レンダリングエンジン: リクエストしたコンテンツを表示させるもの
  4. ネットワーク: HTTPリクエストのようなネットワーク通信を行うもの
  5. UIバックグラウンド: 基本的なUI要素を提供するもの
  6. JavaScriptインタプリタ: JavaScriptをパースし実行するもの
  7. データストレージ: クッキーなど、ある種のデータをハードディスクに保存するもの

Chromeは他のブラウザと異なり、タブごとに別のレンダリングエンジンのインスタンスを持ちます。つまりタブごとに別プロセスを立ち上げています。

レンダリングエンジン

レンダリングエンジンの役割はレンダリングすることです。つまり、リクエストしたコンテンツをブラウザのスクリーンに表示させることです。

FireFoxはGecko、ChromeとSafariはWebKitと呼ばれるレンダリングエンジンを使用しています。

レンダリングエンジンの主なフロー

レンダリングエンジンはネットワークレイヤからリクエストしたコンテンツを受け取ります。
次にレンダリングエンジンの主なフローを示します。

①パース: HTMLを解析し、DOMツリーを構築する
②レンダーツリー構築: レンダーツリーを構築する
③レイアウト: 各要素がスクリーン上のどの位置に表示されるかを決定する
④ペインティング: 描画する

パースとは解析することです。
レンダーツリーとはスタイルの情報が取り込まれたもののことです。

これらの処理は非同期で行われます。つまり、すべてのHTMLが解析されるのを待たずにレンダーツリーを構築し、ネットワークから残りのコンテンツを受信している間に、受信済みのコンテンツを解析・描画します。

ここからは各プロセスについてそれぞれ見ていきます。

パース

パースをするとは文章を意味のある構造に変換するということです。"意味のある"とはコードから理解・利用できるという意味です。通常はパースされた結果は文章の構造を表す木になります。

文法

パースできるフォーマットは語彙と構文ルールから成る文法で記述できる必要があります。これは文脈自由文法と呼ばれます。

パースのプロセス

パースというプロセスは字句解析と構文解析というふたつのサブプロセスに分割できます。
字句解析は入力をトークンに分割するプロセスです。トークンとは意味のあるかたまりのことです。
構文解析はその言語の構文ルールを適用するプロセスです。
パーサーは主に字句解析を行うlexer(あるいはtokenizer)と構文ルールに基づきパースツリーを作成するparserによって構成されます。lexerは空白や改行などの関係ない文字を除くことも行います。
パースのプロセスは反復的です。parderはlexerから新たなトークンを受け取り、それを構文ルールにマッチさせます。もしいずれかのルールにマッチしたら、そのトークンに紐づくノードはパースツリーに追加され、parseは次の新たなトークンをlexerから受け取ります。もし最後までどのルールにもマッチしなかった場合例外が投げられます。これはシンタックスエラーが起こったということです。
作成されたパースツリーは機械語に変換されます。

パーサの種類

パーサには、文法のハイレベルな構造から見ていきそのうちのどれかにマッチさせるトップダウンパーサと文法のローレベルからハイレベルへ当てはめていくボトムアップパーサの二種類があります。

HTMLパーサ

HTMLパーサのしごとはHTMLマークアップをパースしパースツリーを作ることです。HTMLはミスに寛容なため文法を形式的に記述することが難しいです。そのためHTMLをパースするのは簡単でなく、通常のパーサやXMLパーサをそのまま適用することはできません。

DOM

HTMLをパースするとパースツリーという木構造のデータが出力されます。このツリーノードはDOM要素とその属性です。DOMとはDocument Object Modelのことです。DOMはHTMLドキュメントのオブジェクト表現でJavaScriptなどの外部からHTML要素を操作するためのインターフェースです。DOMツリーのルートはDocumentオブジェクトです。
DOMとマークアップはほぼ1対1の関係になります。

パースアルゴリズム

前述のようにHTMLに通常のパーサが使えないのでカスタムパーサを使用します。このアルゴリズムはtokenizationとtree constructionのふたつにわかれます。

tokenizationでは字句解析を行い入力をトークンに分割します。HTMLのトークンとは開始タグ、終了タグ、属性名、属性値などのことです。

tokenizerは新たなトークンを認識し次第それをtree constructerに渡し次の入力を読み込みます。これを入力が終わるまで続けます。

ブラウザエラーの許容範囲

ブラウザで構文エラーが起こることはありません。ブラウザはinvalidなコンテンツを修正しそのまま続行するからです。だからといって壊れたHTMLではなく整った形のHTMLを必ず書くようにしてください。

CSSパース

CSSは文脈自由文法なので通常のパーサでパースすることができます。

スクリプトとスタイルシートの処理順序

Scripts

Webは同期モデルですのでパーサが <script> タグに到達したときにすぐにスクリプトがパース・実行されます。 パース処理はスクリプトの実行が完了するまで停止します。<script>タグにdefer属性を付けると、JavaScriptの読み取りはHTML解析と同時に平行して行われます。HTMLの解析と読み取りが完了してからJavaScriptが実行されます。

Style sheets

考え方としてCSSはDOMの構造を変化させることはないので、DOMの生成を待ったりドキュメントのパースを止める必要はありません。問題はパース中にスクリプトがスタイルの情報を取りに来る場合です。この場合もしその時CSSがまだロード/パースされていなければ、スクリプトは間違った情報にアクセスしてしまいます。FirefoxはCSSがロード中・パース中だった場合、すべてのスクリプトをブロックします。WebKitはそのスクリプトがまだロードされていないCSSにアクセスしようとしている場合のみそのスクリプトをブロックします。

レンダーツリー構築

DOMツリーが構築されている間、ブラウザはレンダーツリーを構築しています。レンダーツリーはビジュアル要素が、それが表示される順に並んでいる木構造です。このツリーの目的はコンテンツの描画を正しい順番で行うことです。

Geckoではレンダーツリーの各要素はframes、WebKitではrendererやrender objectと呼ばれます。レンダラーは自信とその子供をどうレイアウトして描画するかを知っています。

レンダーツリーとDOMツリーの関係

レンダラーはDOMツリーのノードに対応していますが、その関係は1対1ではありません。ビジュアル要素でないものはレンダーツリーには追加されないからです。

ツリー構築の流れ

GeckoにおいてframesはFrame Constructorで作成されます。WebKitにおいてrendererを作成することをattachmentと呼びます。
htmlタグやbodyタグが処理されると、レンダーツリーのルートが作成されます。ツリーの残りの部分はDOMノードの挿入によって作成されます。

CSSのスタイル指定とマッチ

CSSのスタイルしていにはいくつかの方法があります。

  1. スタイル要素や外部CSSファイルのスタイル
p {color: blue}
  1. インラインスタイル属性
<p style="color: blue" />
  1. HTMLのビジュアル属性
<p bgcolor="blue" />

2, 3の場合は対象となるHTML要素に直接スタイルが書かれているので、要素とスタイルのマッチは簡単です。

1の問題はつぎのように対処されます。
CSS ファイルがパースされたあと、各ルールはセレクタに応じていくつかあるハッシュマップに追加されます。ハッシュマップにはid、クラス名、タグ名、それ以外すべてをカバーするgeneral mapなどの種類があります. もしそのルールのセレクタがidで指定していればidのハッシュマップに、クラスならばクラスのハッシュマップに、といった具合です。
この作業によってすべての定義を調べなくても、HTML要素とスタイルのマッチをさせることができます。

ルールを正しいカスケード順序で適用する

ひとつのプロパティに複数の定義があった場合は次の優先度でルールが適用されます。(優先度昇順)

  1. ブラウザのデフォルトスタイル(Browser declarations)
  2. 通常のユーザーCSS(User normal declarations)
  3. Web サイト制作者の通常のCSS(Author normal declarations)
  4. Web サイト制作者のimportant指定付きCSS(Author important declarations)
  5. ユーザー定義のimportant指定付きCSS(User important declarations)

ブラウザのデフォルトスタイルが最も優先度が低く、ユーザー定義のimportant指定つきCSSが最も優先度が高いです。

レイアウト

rendererが作られツリーに追加されたときにはまだその場所やサイズは計算されていません。これらを計算する段階のことをGeckoではreflow、WebKitではlayoutと呼びます。
レイアウトはドキュメントの上から下・左から右の順に実行されますがtable要素は一度の読み込みではレイアウトが決まりません。
座標系はルートとなるフレームの左上が原点です。
レイアウトは再帰的に処理されます。処理はルートレンダラー、つまりHTML要素から始まります。そこから再帰的にすべて、あるいは一部のレンダラーの位置を計算します。
ルートレンダラーの座標は(0, 0)で、大きさはviewport(ブラウザの表示領域)と同じになります。
すべてのレンダラーはlayoutもしくはreflowメソッドをもっていて、それぞれのレンダラーは子のlayoutメソッドを呼び出して位置の計算を行います。

Dirty bit system

小さな変更に対してすべてのレイアウトを計算する必要はないので、ブラウザはdirty bitシステムを使います。レンダラーのレイアウトが必要な場合に自分とその子にdirtyフラグを立てます。

フラグにはdirty、children are dirtyの二種類があります。children are dirtyフラグは自分自身のレイアウトは必要ないが、少なくともひとつの子にレイアウトが必要な場合に立てられるフラグです。

Globalレイアウト

次のような場合には、レンダーツリー全体のレイアウトを行うGlobalレイアウトが起こります。

・fontサイズの変更など、すべてのレンダラーに影響があるスタイル変更を行った場合
・スクリーンサイズに変更があった場合

最適化

全体に影響がなく一部のみ変更されるような場合、レイアウトは実行されません。

レイアウトのプロセス

レイアウトは通常つぎのプロセスを経ます。

  1. 親のレンダラーがそのwidthを決定
  2. 処理が子に移動
    1. 子のレンダラーの位置をセット(子の x, y 座標をセット)
    2. 必要ならばさらに子のレイアウトを呼び出し(dirty フラグが立っていたり, グローバルレイアウトだった場合など)。ここで子の高さが計算される
  3. 親は子の高さを総和し、margin、paddingを加え自分の高さにする。これは親のレンダラーに使われる。
  4. dirtyフラグをfalseにする

ペインティング

描画(painting)の段階ではレンダーツリーが走査され、各レンダラーの内容をスクリーンに表示するためのpaintメソッドが呼び出されます。この段階ではUI infrastructureコンポーネントが使われます。

最適化

ブラウザは何か変更があった際に最小の対応だけで済ませようとします。例えば、ある要素の色を変えた場合は、その要素だけを再描画します。要素の位置を変更した場合は、その要素、子要素、そしておそらくその兄弟要素にもレイアウトと描画が発生します。HTML要素のフォントサイズ変更など、大きな変更があった場合は、キャッシュも使えないので、ツリー全体のレイアウトと再描画が起こります。

レンダリングエンジンのスレッド

レンダリングエンジンはシングルスレッドです。ネットワークなどの一部を除き、すべての処理がひとつのスレッドで実行されます。FirefoxとSafariではこれがブラウザのメインスレッドです。Chromeではそれは各タブのメインスレッドです。

まとめ

ブラウザの主要な機能や主要なコンポーネントを確認したあとに、レンダリングエンジンの主なフローを見てきました。その後、レンダリングエンジンでの各プロセスについてどういったプロセスなのかを確認しました。学んだことを記事に整理することでブラウザの大まかな動作をより理解することができました!

Discussion