zerosant memo

zerosantのメモ帳

最終更新日: 2021.05.12
公開日: 2021.05.10

ブラウザレンダリングの仕組み

レンダリングの前に

HTMLを得るまで

アドレスバーにURLを入力してからHTTPレスポンスを得るまでには、ざっと下記のような流れを踏む。

  1. DNSによりホスト名を解決し、IPアドレスを取得
  2. HTTPによるリソースの取得
    1. TCP/IP接続の確立
    2. TLS接続の確立(HTTPSの場合)
    3. HTTPリクエストの送信、レスポンスを受け取る

レスポンスとしてHTMLを受け取ったところからの流れを以下にまとめた。

レンダリングエンジンとJavaScriptエンジン

ブラウザ内部のソフトウェアコンポーネントのうち、画面表示に大きく関わるコンポーネントがレンダリングエンジンとJavaScriptエンジン。

レンダリングエンジン

HTMLの描画エンジン。Webページのレンダリングのみを担当。HTMLや画像ファイルやCSS, Javascriptなどのリソースを読み取り、それを画面上の実際のピクセルとして描画する。 ブラウザの他にも、メールクライアントのHTMLメール表示や、アプリのWebViewなどに組み込まれている。

JavaScriptエンジン

JavaScript実行環境を提供するコンポーネント。DOMツリーやCSSOMツリーなどの内部オブジェクトやAPIに対して、JavaScriptからアクセスできるようにするバインディングが提供される。

ブラウザ レンダリングエンジン JavaScriptエンジン
Google Chrome Blink V8
Internet Explore Trident Chakra
Microsoft Edge EdgeHTML Chakra
Mozilla Firefox Gecko SpiderMonkey
Safari Webkit Nitro

レンダリングの大きな流れ

  1. Loading: リソースの読み込み
    1. Download
    2. Parse
  2. Scripting
  3. Rendering
    1. Calcuralte Style
    2. Layout
  4. Painting
    1. Paint
    2. Rasterize
    3. Composite Layers

1-1 Download

リソースのダウンロードを行う。まずはHTMLをダウンロードする。

1-2 Parse

ダウンロードしたHTMLをレンダリングエンジンが解析(Parse)し、ブラウザの内部表現であるDOMツリーに変換する。この変換過程で、ツリー内に含まれる画像やCSS, JavaScriptなどのドキュメントに紐づくリソースの取得、ダウンロードを行う。(<link>, <img>, <script>で宣言されるリソース)

CSSもまたダウンロードされたのちレンダリングエンジンにより解析され、CSSOMツリーに変換する。

HTMLの解析

DOMとは、HTMLの文書を表現するオブジェクトのこと。、HTML文書をツリー状のデータ構造として扱い、それをプログラムから参照したり操作したりするためのデータ構造やインタフェース ( API ) を定義したもの。DOMツリーは、レンダリングエンジンが利用する木構造を持つ内部表現。HTMLがDOMツリーに変換されるまでの工程は以下のとおり。

  1. 字句解析: トークンのリスト化
  2. 構文解析: 構文木の構築
  3. DOMツリー構築(構文木内のJavaScriptを実行しつつ)

字句解析

ただの文字列であるHTMLを、意味的に1つの塊となった文字列の集合(トークンのリスト)に変換する。

構文解析

トークンのリストに分解されたものを使用して、構文木を構築する。構文木は木構造のデータ。

DOMツリーの構築

構文木に含まれるJavaScriptを同期的に実行しつつ、DOMツリーを構築する。

たとえば、document.write('SomeText');というJavaScriptがあった場合は、その実行を待って書き出された文字列を同期的にトークンの列に追加する。

<div>
  <h1>Title</h1>
  <p>Paragraph</p>
</div>

(DOMツリーの図)

CSSの解析

CSSを解析して、CSSOM(CSS Object Model)と呼ばれるオブジェクトツリーを構築する。

h1 {
  font-size: 21px;
}

#title {
  color: red;
  background-color: white;
  width: 200px;
}

(CSSOMツリーの図)

2. Scripting

ダウンロードされたJavaScriptを、レンダリングエンジンはJavaScriptエンジンに引き渡して実行させる。実行の過程は以下のとおり。

  1. 字句解析: トークン列に変換
  2. 構文解析: 抽象構文木に変換
  3. コンパイル: 実行可能コードに変換
  4. 実行

字句解析と構文解析

JavaScriptコード

console.log('Hello, world!');

トークン列(ラベル付された文字列のリスト)

抽象構文木(JavaScriptの文法に沿った形で表現される木構造のデータ)

コンパイル

抽象構文木を実行可能な形式に変換する。ここでの実行可能な形式は、JavaScriptエンジンにより異なる。

  • 処理系内部の仮想マシン用のコードに変換
    • 返還後のコードを実行するための仮想マシンを用意し、そのマシンが実行できるコードを生成。
  • JITコンパイルによる機械語への変換
    • その処理系が動作しているマシンのCPUが直接解釈できる機械語に変換。
    • 実行時にその場でコンパイルすることに由来(Just In Time)
    • V8もこれ
  • その両方(トレーシングJIT)

3-1. Calculate Style

DOMツリーの全てのDOM要素に対し、どのようなCSSプロパティを当てるか計算する。 CSSOMのすべてを参照し、CSSOM:DOMの総当たりでセレクタマッチングする。 このとき詳細度も算出する。

セレクタマッチング

セレクタマッチングは、セレクタの右から行う。

body > .container > .button {
}

↑の場合、下記の順番で検証する。

  1. .button はあるか
  2. その親に .container はあるか
  3. さらにその親の要素名は body dearuka

3-2. Layout

DOMツリーのすべてのノードの視覚的レイアウト情報の計算を行う。(描画はまだしない) 視覚的レイアウト情報に含まれるのは、

  • width, height
  • margin
  • padding
  • position
  • z軸の位置

4-1. Paint

内部の低レベルな2Dグラフィックエンジン向けの命令列を生成する。(ex: Skia、CoreGraphics)

4-2. Rasterize

Paint で生成された命令を用いて、ピクセル(ビットマップ)を描画する。このとき、レイヤーという単位で1枚ずつ描画する。 レイヤーが生成される条件は、下記のとおり。

  • position: absoluteなスタイルが適用されている
  • position: fixedなスタイルが適用されている
  • transform: translate3d()などのGPUで描画・合成されるプロパティを持っている
  • opacityが指定されており、かつ投下して背景のコンテンツを表示する必要がある

なぜレイヤーを生成するのか

再レンダリングのときに再利用できる場合があり、素早く再レンダリングできるため。例えば、スクロールするときは、スクロール分だけコンテンツの表示される位置ずらせばよく、コンテンツの描画そのものは変更する必要がないため、以前描画したレイヤーが再利用される。

4-3. Composit Layers

レイヤを合成して最終的なレンダリング結果を生成する。基本的にはCPUによって合成される。 下記の場合はGPUによって合成される。

  • transformCSSで3D変形関数を指定している。
    • matrix3D, scale3D, rotateZ, translateZ など
  • 特定のCSSFilterを使用している

再レンダリング

描画が終わってからも、JavaScriptの実行やドキュメント内のイベントによってレンダリングは繰り返される。 DOM操作を行えば、3-1 Calculate Style から行われる場合もあれば、4-2 Rasterize からで十分な場合もある。 また、Ajaxで外部リソースを取得した場合など、1-1 Download から再実行される場合もある。