最新ウェブブラウザの舞台裏
序文
WebKit と Gecko の内部オペレーションに関するこの包括的な入門情報は、イスラエルのデベロッパー Tali Garsiel 氏による多くの研究の成果です。数年にわたり、ブラウザ内部に関するすべての公開データを確認し、ウェブブラウザのソースコードを読むことに多くの時間を費やしました。彼女は次のように書いています。
ウェブ デベロッパーは、ブラウザ操作の内部構造を学ぶことで、より的確な意思決定を行い、開発のベスト プラクティスの背後にある理由を知ることができます。これはかなり長いドキュメントですが、時間をかけてじっくり読むことをおすすめします。やったら嬉しいよ。
Chrome デベロッパー リレーションズ、Paul Irish
はじめに
ウェブブラウザは、最も広く使用されているソフトウェアです。この入門編では その仕組みを解説しますアドレスバーに「google.com
」と入力してから、ブラウザの画面に Google のページが表示されるまで、動作を確認します。
取り上げるブラウザ
現在、パソコンで使用されている主なブラウザは、Chrome、Internet Explorer、Firefox、Safari、Opera の 5 つです。モバイルの主なブラウザは、Android Browser、iPhone、Opera Mini、Opera Mobile、UC Browser、Nokia S40/S60 ブラウザ、Chrome です。Opera ブラウザを除く、これらはすべて WebKit をベースにしています。オープンソース ブラウザである Firefox と Chrome、Safari(一部オープンソース)を例に説明します。StatCounter の統計(2013 年 6 月現在)によると、Chrome、Firefox、Safari は、世界のパソコンのブラウザ使用の約 71% を占めています。モバイルでは、Android ブラウザ、iPhone、Chrome が使用量の約 54% を占めています。
ブラウザの主な機能は
ブラウザの主な機能は、選択したウェブリソースをサーバーからリクエストしてブラウザ ウィンドウに表示することで、そのウェブリソースを提示することです。通常、リソースは HTML ドキュメントですが、PDF、画像、その他の種類のコンテンツである場合もあります。リソースの場所は、ユーザーが URI(Uniform Resource Identifier)を使用して指定します。
ブラウザによる HTML ファイルの解釈と表示の方法は、HTML と CSS の仕様で規定されています。 これらの仕様は、ウェブの標準化団体である W3C(World Wide Web Consortium)によって管理されています。長年にわたり、ブラウザは仕様の一部のみに準拠し、独自の拡張機能を開発してきました。その結果、ウェブ作成者に互換性の深刻な問題が生じました。現在では、ほとんどのブラウザがほぼそのまま仕様になっています。
ブラウザのユーザー インターフェースには多くの共通点があります。一般的なユーザー インターフェース要素は次のとおりです。
- URI を挿入するためのアドレスバー
- 戻るボタンと進むボタン
- ブックマークのオプション
- 現在のドキュメントを更新または読み込みを停止するための更新ボタンと停止ボタン
- ホームページに移動するホームボタン
不思議なことに、ブラウザのユーザー インターフェースは正式な仕様で規定されているわけではなく、長年の経験から培われた優れたプラクティスや、各ブラウザが互いを模倣して作り上げたものです。HTML5 仕様では、ブラウザに必要な UI 要素が定義されるわけではありませんが、いくつかの共通要素が列挙されています。その中には、アドレスバー、ステータスバー、ツールバーなどがあります。 もちろん、Firefox のダウンロード マネージャーなど、特定のブラウザに固有の機能もあります。
インフラストラクチャの概要
ブラウザの主なコンポーネントは次のとおりです。
- ユーザー インターフェース: アドレスバー、戻る/進むボタン、ブックマーク メニューなどです。リクエストされたページが表示されるウィンドウを除く、ブラウザ表示のすべての部分です。
- ブラウザ エンジン: UI とレンダリング エンジンの間のアクションをマーシャリングします。
- レンダリング エンジン: リクエストされたコンテンツを表示します。たとえば、リクエストされたコンテンツが HTML の場合、レンダリング エンジンは HTML と CSS を解析し、解析したコンテンツを画面に表示します。
- ネットワーキング: HTTP リクエストなどのネットワーク呼び出し用。プラットフォームに依存しないインターフェースの背後で、プラットフォームごとに異なる実装を使用します。
- UI バックエンド: コンボボックスやウィンドウなどの基本的なウィジェットの描画に使用されます。このバックエンドは、プラットフォーム固有ではない汎用インターフェースを公開します。その下ではオペレーティング システムのユーザー インターフェース メソッドが使用されています。
- JavaScript インタープリタ。JavaScript コードの解析と実行に使用されます。
- データ ストレージ。これは永続性レイヤです。ブラウザでは、Cookie など、あらゆる種類のデータをローカルに保存しなければならない場合があります。ブラウザは、localStorage、IndexedDB、WebSQL、FileSystem などのストレージ メカニズムもサポートしています。
Chrome などのブラウザは、レンダリング エンジンの複数のインスタンス(タブごとに 1 つ)を実行することに注意してください。各タブは別々のプロセスで実行されます。
レンダリング エンジン
レンダリング エンジンの役割は、レンダリング、つまりリクエストされたコンテンツをブラウザ画面に表示することです。
デフォルトでは、レンダリング エンジンは HTML と XML のドキュメントと画像を表示できます。プラグインや拡張機能を使用して、他のタイプのデータを表示できます。たとえば、PDF ビューア プラグインを使用して PDF ドキュメントを表示する場合などです。ただし、この章では、CSS を使用してフォーマットされた HTML と画像を表示するという、主なユースケースに焦点を当てます。
ブラウザによって使用されるレンダリング エンジンは異なります。Internet Explorer は Trident、Firefox は Gecko、Safari は WebKit を使用します。Chrome と Opera(バージョン 15 以降)は、WebKit のフォークである Blink を使用します。
WebKit はオープンソースのレンダリング エンジンです。Linux プラットフォームのエンジンとしてスタートし、Mac と Windows をサポートするように Apple が修正しました。
メインフロー
レンダリング エンジンは、ネットワーク レイヤからリクエストされたドキュメントのコンテンツの取得を開始します。通常は 8 KB のチャンクに分割されます。
その後のレンダリング エンジンの基本的なフローは次のとおりです。
レンダリング エンジンが HTML ドキュメントの解析を開始し、要素を「コンテンツ ツリー」と呼ばれるツリー内の DOM ノードに変換します。エンジンは、外部 CSS ファイルとスタイル要素の両方のスタイルデータを解析します。スタイル設定情報と HTML の視覚的な指示を使用して、別のツリー(レンダリング ツリー)が作成されます。
レンダリング ツリーには、色や寸法などの視覚的な属性を持つ長方形が含まれます。これらの長方形は、画面に正しい順序で表示されます。
レンダー ツリーが構築されると、「レイアウト」プロセスに進みます。つまり、画面上の正確な座標を各ノードで指定します。 次のステージはペインティングです。レンダリング ツリーを走査し、UI バックエンド レイヤを使用して各ノードをペイントします。
なお、このプロセスは段階的に行われるため、ユーザー エクスペリエンスを向上させるため、レンダリング エンジンは可能な限り早く画面にコンテンツを表示しようとします。すべての HTML が解析されるまで待機せず、レンダリング ツリーの作成とレイアウトを開始します。コンテンツの一部が解析されて表示される間、プロセスはネットワークから引き続き送信される残りのコンテンツで続行されます。
メインフローの例
図 3 と 4 を見ると、WebKit と Gecko の用語は若干異なりますが、フローは基本的に同じです。
Gecko では、視覚的にフォーマットされた要素のツリーを「フレームツリー」と呼んでいます。各要素はフレームです。WebKit では「レンダリング ツリー」という用語が使用されており、これは「レンダリング オブジェクト」で構成されています。WebKit では要素の配置に「レイアウト」という用語を使用しますが、Gecko ではこれを「Reflow」と呼んでいます。「アタッチメント」とは、DOM ノードと視覚情報を接続してレンダリング ツリーを作成することを指す WebKit の用語です。セマンティック以外のわずかな違いとして、Gecko には HTML と DOM ツリーの間に追加のレイヤがあります。これは「コンテンツ シンク」と呼ばれ、DOM 要素を作成するためのファクトリです。フローの各部分について説明します。
解析 - 一般
解析はレンダリング エンジン内で非常に重要なプロセスであるため、もう少し深く掘り下げます。解析について簡単に紹介しましょう。
ドキュメントを解析するということは、コードで使用できる構造に変換することです。解析結果は通常、ドキュメントの構造を表すノードのツリーになります。これは、解析ツリーまたは構文ツリーと呼ばれます。
たとえば、式 2 + 3 - 1
を解析すると、次のツリーが返されます。
Grammar
解析は、ドキュメントが従う構文ルール(記述された言語や形式)に基づいて行われます。解析できる形式はすべて、語彙と構文のルールで構成される決定論的な文法を持つ必要があります。これは「文脈自由文法」と呼ばれます。人間の言語はこのような言語ではないため、従来の解析手法では解析できません。
パーサーとレキサーの組み合わせ
解析は、字句解析と構文解析という 2 つのサブプロセスに分けられます。
語彙分析は、入力をトークンに分割するプロセスです。トークンは言語の語彙、つまり有効な構成要素のコレクションです。人間の言語では、その言語の辞書に出てくるすべての単語で構成されます。
構文解析とは、言語の構文ルールを適用することです。
パーサーは通常、入力を有効なトークンに分割する「レキサー」(トークナイザー)と、言語の構文ルールに従ってドキュメントの構造を分析して解析ツリーを構築する「パーサー」という 2 つのコンポーネントに作業を分割します。
レキサーは、空白や改行などの無関係な文字を削除する方法を理解しています。
解析プロセスは反復的です。パーサーは通常、レキサーに新しいトークンを要求し、そのトークンを構文ルールのいずれかと照合しようとします。ルールが一致すると、トークンに対応するノードが解析ツリーに追加され、パーサーは別のトークンを要求します。
一致するルールがない場合、パーサーはトークンを内部的に保存し、内部的に保存されているすべてのトークンに一致するルールが見つかるまでトークンを要求し続けます。ルールが見つからない場合、パーサーは例外を発生させます。これは、ドキュメントが有効ではなく、構文エラーが含まれていたことを意味します。
Translation
多くの場合、解析ツリーは最終的な成果物ではありません。解析は翻訳でよく使用されます。つまり、入力ドキュメントを別の形式に変換します。たとえば、コンパイルがあります。ソースコードをマシンコードにコンパイルするコンパイラは、まずソースコードを解析して解析ツリーに変換し、次にこのツリーをマシンコード ドキュメントに変換します。
解析の例
図 5 では、数式から解析ツリーを作成しました。簡単な数学的言語を定義して、解析のプロセスを見てみましょう。
構文:
- 言語の構文構成要素は、式、項、演算です。
- この言語には任意の数の表現を含めることができます。
- 式は、「項」の後に「演算」が続き、さらに別の項が続くものとして定義されます。
- 演算はプラストークンまたはマイナストークン
- 用語は整数のトークンまたは式
入力 2 + 3 - 1
を分析します。
ルールに最初に一致する部分文字列は 2
です。ルール 5 によれば、これは単語です。2 番目の一致は 2 + 3
です。これは 3 番目のルール(項の後、演算の後に別の項が続く)に一致します。次の一致は、入力の最後でのみヒットします。
2 + 3
は項であることがわかっているため、2 + 3 - 1
は式です。項の後に演算が続き、その後に別の項が続きます。2 + +
はどのルールにも一致しないため、無効な入力です。
語彙と構文の正式な定義
語彙は通常、正規表現で表現します。
たとえば、言語は次のように定義されます。
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
ご覧のとおり、整数は正規表現で定義されます。
構文は通常、BNF という形式で定義されます。使用する言語は次のように定義されます。
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
前述したように、文法が文脈自由文法であれば、その言語は通常のパーサーで解析できます。 文脈自由文法の直感的な定義は、BNF で完全に表現できる文法です。正式な定義については、コンテキスト自由文法に関するウィキペディアの記事をご覧ください。
パーサーの種類
パーサーには、トップダウン パーサーとボトムアップ パーサーの 2 種類があります。わかりやすい説明として、トップダウン パーサーは構文の上位レベルの構造を調べ、ルールの一致を見つけようとします。ボトムアップ パーサーは、入力から始まり、低レベルのルールから高レベルのルールが満たされるまで、徐々にその入力を構文ルールに変換します。
2 種類のパーサーでこの例がどのように解析されるかを見ていきましょう。
トップダウン パーサーは上位レベルのルールから始まります。つまり、2 + 3
を式として識別します。次に、2 + 3 - 1
を式として識別します(式を特定するプロセスが進化し、他のルールと一致しますが、開始点は最上位のルールです)。
ボトムアップ パーサーは、ルールが一致するまで入力をスキャンします。一致する入力がルールに置き換えられます。これは入力が終わるまで繰り返されます。 部分的に一致した式はパーサーのスタックに配置されます。
このタイプのボトムアップ パーサーは、Shift-reduce パーサーと呼ばれます。入力が右にシフトされ(ポインタが入力の先頭を指し、最初に右に移動することを想像する)、構文ルールに徐々に減らされるためです。
パーサーの自動生成
パーサーを生成できるツールがあります。言語の文法(語彙と構文のルール)をフィードすると、実用的なパーサーが生成されます。パーサーを作成するには解析に関する深い理解が必要ですが、最適化されたパーサーを手作業で作成することは容易ではないため、パーサー ジェネレータは非常に便利です。
WebKit では、レキサーを作成するための Flex と、パーサーを作成するための Bison という 2 つのよく知られているパーサー ジェネレータを使用します(Lex と Yacc という名前で出会うことがあります)。Flex 入力は、トークンの正規表現定義を含むファイルです。Bison の入力は、BNF 形式の言語構文ルールです。
HTML パーサー
HTML パーサーの役割は、HTML マークアップを解析して解析ツリーにすることです。
HTML の文法
HTML の語彙と構文は、W3C 組織が作成した仕様で定義されています。
解析の概要で説明したように、文法の構文は BNF のような形式を使用して正式に定義できます。
残念ながら、従来のパーサーのトピックはどれも HTML には当てはまりません(CSS や JavaScript の解析には、単なる娯楽として取り上げているわけではありません)。 HTML は、パーサーに必要なコンテキスト自由文法では簡単に定義できません。
HTML を定義するには正式な形式である DTD(ドキュメント タイプ定義)がありますが、これは文脈自由文法ではありません。
これは一見奇妙に思えます。HTML はむしろ XML に近いということです。利用可能な XML パーサーは多数あります。HTML には XML 形式の XHTML があります。大きな違いは何でしょうか。
違いは、HTML アプローチの方が「寛容」である点です。特定のタグを省略して(後で暗黙的に追加することも、開始タグや終了タグなども省略するなど)、特定のタグを省略できます。XML の堅牢で要求の厳しい構文とは異なり、全体的に「ソフト」な構文になっています。
些細なことに思えるだけでも、大きな違いが生まれます。 HTML が広く普及している主な理由はそこにあります。HTML ならミスが許容され、ウェブ作成者の作業が楽になります。 その一方で、正式な文法を書くのは困難です。つまり、HTML の文法はコンテキストに依存しないため、従来のパーサーでは簡単に解析できません。XML パーサーで HTML を解析できません。
HTML DTD
HTML 定義は DTD 形式です。この形式は、SGML ファミリーの言語を定義するために使用されます。この形式には、使用可能なすべての要素とその属性、階層の定義が含まれます。前述のように、HTML DTD は文脈自由文法ではありません。
DTD にはいくつかのバリエーションがあります。strict モードは仕様だけに準拠しますが、他のモードでは、過去にブラウザで使用されていたマークアップをサポートしています。古いコンテンツとの下位互換性が目的です。現在の厳密な DTD は www.w3.org/TR/html4/strict.dtd にあります。
DOM
出力ツリー(「解析ツリー」)は、DOM 要素と属性ノードのツリーです。DOM は、ドキュメント オブジェクト モデルの略です。HTML ドキュメントのオブジェクト表現であり、JavaScript などの HTML 要素の外部へのインターフェースです。
ツリーのルートは「Document」オブジェクトです。
DOM は、マークアップとほぼ 1 対 1 の関係にあります。次に例を示します。
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
このマークアップは次の DOM ツリーに変換されます。
HTML と同様に、DOM は W3C 組織によって指定されています。www.w3.org/DOM/DOMTR をご覧ください。これは、ドキュメント操作の一般的な仕様です。特定のモジュールでは、HTML 固有の要素を記述します。HTML の定義については、www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html をご覧ください。
ツリーが DOM ノードを含むということは、ツリーはいずれかの DOM インターフェースを実装する要素で構成されるということです。ブラウザでは、ブラウザが内部的に使用する他の属性を持つ具体的な実装を使用します。
解析アルゴリズム
前のセクションで説明したように、通常のトップダウン パーサーやボトムアップ パーサーでは HTML を解析できません。
その理由は次のとおりです。
- 言語の寛容さ。
- ブラウザは従来のエラー耐性を備えており、無効な HTML のよく知られたケースにも対応できます。
- 解析プロセスはリエントラントです。他の言語では、解析中にソースは変更されませんが、HTML では動的コード(
document.write()
呼び出しを含むスクリプト要素など)によってトークンが追加されることがあるため、解析プロセスで実際に入力が変更されます。
通常の解析技術を使用できないため、ブラウザは HTML を解析するカスタム パーサーを作成します。
解析アルゴリズムは、HTML5 仕様に詳しく記載されています。このアルゴリズムは、トークン化とツリー構築という 2 つのステージで構成されています。
トークン化は語彙分析であり、入力をパースしてトークンに変換します。HTML トークンには、開始タグ、終了タグ、属性名、属性値が含まれます。
トークナイザはトークンを認識してツリー コンストラクタに渡し、次の文字を使用して次のトークンを認識し、入力の終わりまでこれを繰り返します。
トークン化アルゴリズム
アルゴリズムの出力は HTML トークンです。アルゴリズムはステートマシンとして表現されます。各状態は入力ストリームの 1 つ以上の文字を使用し、その文字に従って次の状態を更新します。この決定は、現在のトークン化状態とツリー構築状態の影響を受けます。つまり、同じ消費文字でも、現在の状態に応じて、次の状態では異なる結果が生成されます。 このアルゴリズムは複雑すぎて完全に説明できないので、この原則を理解するのに役立つ簡単な例を見てみましょう。
基本的な例 - 次の HTML をトークン化します。
<html>
<body>
Hello world
</body>
</html>
初期状態は「データ状態」です。<
文字が検出されると、状態は「タグ開始状態」に変更されます。a-z
文字を消費すると「開始タグトークン」が作成され、状態は「タグ名状態」に変わります。>
文字が消費されるまでこの状態のままです。各文字が新しいトークン名に付加されます。この例では、作成されるトークンは html
トークンです。
>
タグに達すると、現在のトークンが発行され、状態が「データ状態」に戻ります。<body>
タグも同じ手順で処理されます。ここまでの時点で、html
タグと body
タグが出力されています。これで「データ状態」に戻ります。Hello world
の H
文字を消費すると、文字トークンの作成と出力が発生します。この処理は </body>
の <
に達するまで続きます。Hello world
の各文字に対応する文字トークンを出力します。
こうして「タグ開始状態」に戻ります。
次の入力 /
を使用すると、end tag token
が作成され、「タグ名の状態」に移動します。ここでも、>
に達するまでこの状態を維持します。新しいタグトークンが発行されると、データ状態に戻ります。</html>
入力は前のケースと同様に処理されます。
ツリー構築アルゴリズム
パーサーが作成されると、Document オブジェクトが作成されます。ツリー構築段階で、ルートに Document を持つ DOM ツリーが変更され、要素が追加されます。 トークナイザによって出力された各ノードは、ツリー コンストラクタによって処理されます。この仕様では、トークンごとに、関連する DOM 要素を定義し、そのトークンに対して作成される DOM 要素を定義します。要素は DOM ツリーに追加され、開いた要素のスタックにも追加されます。このスタックは、ネストの不一致と閉じていないタグを修正するために使用されます。 アルゴリズムはステートマシンとしても記述されます。この状態は「挿入モード」と呼ばれます。
入力例のツリー構築プロセスを見てみましょう。
<html>
<body>
Hello world
</body>
</html>
ツリー構築ステージへの入力は、トークン化ステージからの一連のトークンです。最初のモードは「初期モード」です。「html」トークンを受け取ると「before html」モードに移行し、そのモードでトークンが再処理されます。 これにより、ルート Document オブジェクトに追加される HTMLHTMLElement 要素が作成されます。
状態は「before head」に変更されます。「body」トークンが受信されます。HTMLHeadElement は暗黙的に作成されますが、「head」トークンはなく、ツリーに追加されます。
次に in head モードに続いて after head モードに移ります。body トークンが再処理され、HTMLBodyElement が作成、挿入され、モードが "in body" に移行されます。
これで、「Hello world」文字列の文字トークンが受信されます。最初の文字で「Text」ノードの作成と挿入が行われ、他の文字はそのノードに追加されます。
本文終了トークンを受け取ると、「after body」モードに移行します。html 終了タグを受け取ると、「after after body」モードに移動します。ファイルの終わりのトークンを受け取ると、解析が終了します。
解析が終了したときのアクション
この段階で、ブラウザはドキュメントをインタラクティブとしてマークし、「遅延」モードのスクリプト(ドキュメントの解析後に実行する必要があるスクリプト)の解析を開始します。その後、ドキュメントの状態は「complete」に設定され、「load」イベントが発生します。
トークン化とツリー構築のすべてのアルゴリズムについては、HTML5 仕様をご覧ください。
ブラウザのエラー許容度
HTML ページで「無効な構文」エラーが表示されることはありません。 ブラウザは無効なコンテンツを修正して編集を続けます。
次の HTML の例を見てみましょう。
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
約 100 万件のルールに違反したに違いありません(「mytag」は標準タグではなく、「p」要素と「div」要素の誤ったネストなど)が、ブラウザには正しく表示され、何も問題はありません。 多くのパーサーコードが、HTML 作成者の誤りを修正しています。
エラー処理は各ブラウザでかなり一貫していますが、驚くほどこれは HTML 仕様には含まれていません。ブックマークや戻る/進むボタンのように、長年にわたってブラウザで開発されたものです。無効な HTML 構造が多くのサイトで繰り返されていることがわかっており、ブラウザは他のブラウザに適合する方法で修正を試みます。
HTML5 仕様では、こうした要件の一部が定義されています。(WebKit は、HTML パーサー クラスの冒頭のコメントに、この点をわかりやすくまとめています)。
パーサーはトークン化された入力を解析してドキュメント ツリーを構築し、ドキュメントの形式が正しい場合、解析は簡単です。
残念ながら、形式が適切でない多くの HTML ドキュメントを処理する必要があるため、パーサーはエラーに耐えなければなりません。
少なくとも次のエラー状態に対処する必要があります。
- 追加する要素は、外側のタグの内部で明示的に禁止されています。この場合、その要素を禁止するタグまでのすべてのタグを閉じて、後から追加する必要があります。
- 要素を直接追加することはできません。ドキュメントの作成者が、間にタグの一部を忘れた(または、中間のタグがオプションである)可能性があります。たとえば、HTML HEAD BODY TBODY TR TD LI(何か忘れていないか)。
- インライン要素内にブロック要素を追加します。1 つ上のブロック要素までのインライン要素をすべて閉じます。
- それでも問題が解決しない場合は、要素を追加できるようになるまで要素を閉じるか、タグを無視します。
WebKit のエラー耐性の例をいくつか見てみましょう。
</br>
(<br>
の代わりに使用)
一部のサイトでは、<br>
ではなく </br>
が使用されています。IE と Firefox との互換性を確保するため、WebKit ではこれを <br>
のように扱います。
コード:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
エラー処理は内部で行われ、ユーザーには表示されません。
迷子のテーブル
迷路のテーブルは、別のテーブル内にあるが、テーブルセルの中ではないテーブルです。
次に例を示します。
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
WebKit は、階層を 2 つの兄弟テーブルに変更します。
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
コード:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
WebKit は現在の要素のコンテンツにスタックを使用します。つまり、内側のテーブルを外側のテーブル スタックからポップします。これで、テーブルは兄弟テーブルになります。
ネストされたフォーム要素
ユーザーが別のフォーム内にフォームを挿入した場合、2 番目のフォームは無視されます。
コード:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
タグ階層が深すぎる
コメントが物語っています。
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{
unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}
html または body の終了タグが配置されていない
繰り返しになりますが、コメントはそれ自体を物語っています。
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
したがって、ウェブ作成者は、WebKit のエラー耐性コード スニペットの例として表示されるのでない限り、正しい形式の HTML を記述するように注意してください。
CSS 解析
「はじめに」で解析の概念を覚えていますか?HTML とは異なり、CSS は文脈自由文法であり、「はじめに」で説明したタイプのパーサーを使用して解析できます。実際、CSS 仕様では、CSS の語彙と構文の文法が定義されています。
例をいくつか見てみましょう。
語彙文法(語彙)は、各トークンの正規表現によって定義されます。
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
「ident」は「識別子」の略で、クラス名と同様です。「name」は要素 ID(「#」で参照)です。
構文文法は BNF で記述されます。
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
説明:
ルールセットの構造は次のとおりです。
div.error, a.error {
color:red;
font-weight:bold;
}
div.error
と a.error
はセレクタです。中かっこ内の部分には、このルールセットによって適用されるルールが含まれています。この構造は、次の定義で正式に定義されています。
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
つまり、ルールセットはセレクタです。必要に応じて、カンマとスペースで区切られた複数のセレクタです(S は空白文字を表します)。ルールセットには中かっこが含まれ、その中に宣言か、必要に応じてセミコロンで区切った複数の宣言を含めることができます。「宣言」と「セレクタ」は、次の BNF 定義で定義されます。
WebKit CSS パーサー
WebKit は、Flex と Bison のパーサー ジェネレータを使用して、CSS 文法ファイルからパーサーを自動的に作成します。パーサーの概要で説明したとおり、Bison はボトムアップの Shift-reduce パーサーを作成します。 Firefox は、手動で記述されたトップダウン パーサーを使用します。どちらの場合も、各 CSS ファイルは解析されてスタイルシート オブジェクトに変換されます。各オブジェクトには CSS ルールが含まれています。CSS ルール オブジェクトには、セレクタ オブジェクトと宣言オブジェクトのほか、CSS の文法に対応するその他のオブジェクトが含まれます。
スクリプトとスタイルシートの処理順序
スクリプト
ウェブのモデルは同期的です。作成者は、パーサーが <script>
タグに到達するとすぐにスクリプトが解析されて実行されることを想定しています。ドキュメントの解析は、スクリプトが実行されるまで停止します。スクリプトが外部にある場合は、最初にリソースをネットワークからフェッチする必要があります。この操作も同期的に行われ、リソースがフェッチされるまで解析は停止されます。
これは長年にわたってこのモデルであり、HTML4 と 5 の仕様でも規定されています。作成者はスクリプトに「defer」属性を追加できます。その場合、ドキュメントの解析は停止されず、ドキュメントの解析後に実行されます。HTML5 には、スクリプトを非同期としてマークするオプションが追加され、解析された後、別のスレッドで実行されます。
投機的解析
WebKit と Firefox のどちらもこの最適化を行います。スクリプトの実行中に、別のスレッドがドキュメントの残りの部分を解析し、ネットワークから読み込む必要がある他のリソースを見つけて読み込みます。このようにして、並列接続でリソースを読み込むことができ、全体的な速度が向上します。注: 投機的パーサーは、外部スクリプト、スタイルシート、画像などの外部リソースへの参照のみを解析します。DOM ツリーは変更されません。これはメイン パーサーに委ねられます。
スタイルシート
一方、スタイル シートはモデルが異なります。 概念的には、スタイルシートによって DOM ツリーが変更されることはないため、スタイルシートの解析を待たずにドキュメントの解析を停止する必要はありません。ただし、ドキュメントの解析段階でスクリプトがスタイル情報を要求するという問題があります。スタイルの読み込みと解析が完了していない場合、スクリプトの回答が正しくないため、多くの問題が生じている可能性があります。 これはエッジケースのように見えますが、非常に一般的です。Firefox では、読み込みと解析が進行中のスタイルシートがある場合、すべてのスクリプトがブロックされます。WebKit は、スタイル プロパティが読み込まれていないときに影響を受ける可能性がある特定のスタイル プロパティにアクセスしようとした場合にのみ、スクリプトをブロックします。
レンダー ツリーの構築
DOM ツリーの作成中に、ブラウザは別のツリー(レンダリング ツリー)を構築します。このツリーは、表示順に視覚要素で構成されます。ドキュメントを視覚的に表したものです。このツリーの目的は、コンテンツを正しい順序でペイントできるようにすることです。
Firefox では、レンダー ツリー内の要素を「フレーム」と呼んでいます。WebKit ではレンダラまたはレンダー オブジェクトという用語を使用します。
レンダラは、自身と子をレイアウトしてペイントする方法を認識しています。
レンダラの基本クラスである WebKit の RenderObject クラスには、次の定義があります。
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
CSS2 の仕様で説明されているように、各レンダラは通常、ノードの CSS ボックスに対応する長方形の領域を表します。これには、幅、高さ、位置などのジオメトリ情報が含まれます。
ボックスタイプは、ノードに関連する style 属性の「display」値の影響を受けます(スタイルの計算セクションをご覧ください)。 次の WebKit コードは、display 属性に応じて、DOM ノード用に作成するレンダラのタイプを決定するためのものです。
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
要素のタイプも考慮されます。たとえば、フォーム コントロールやテーブルには特殊なフレームがあります。
WebKit では、要素で特別なレンダラを作成する場合、createRenderer()
メソッドがオーバーライドされます。レンダラは、非ジオメトリ情報を含むスタイル オブジェクトを指します。
レンダリング ツリーと DOM ツリーの関係
レンダラは DOM 要素に対応していますが、関係は 1 対 1 ではありません。非視覚的 DOM 要素はレンダリング ツリーに挿入されません。「head」要素などがこれに該当します。また、表示値が「none」に割り当てられた要素はツリーに表示されません(一方、表示の値が「hidden」である要素はツリーに表示されます)。
複数の視覚的オブジェクトに対応する DOM 要素があります。これらは通常、1 つの長方形では説明できない複雑な構造の要素です。たとえば、「select」要素には 3 つのレンダラがあります。1 つは表示領域用、1 つはプルダウン リスト ボックス用、もう 1 つはボタン用です。 また、1 行では幅が足りず、テキストが複数行に分割されている場合は、新しい行が別のレンダラとして追加されます。
複数のレンダラのもう一つの例は、破損した HTML です。CSS の仕様によると、インライン要素にはブロック要素のみ、またはインライン要素のみを含める必要があります。混合コンテンツの場合は、インライン要素をラップするために匿名のブロック レンダラが作成されます。
一部のレンダリング オブジェクトは DOM ノードに対応していますが、ツリー内の同じ位置にはありません。浮動小数点数と絶対位置の要素は、フローから外れてツリーの別の部分に配置され、実際のフレームにマッピングされます。プレースホルダ フレームは、表示されるはずの場所です。
ツリー構築のフローは、
Firefox では、プレゼンテーションは DOM 更新のリスナーとして登録されます。プレゼンテーションはフレームの作成を FrameConstructor
に委譲し、コンストラクタはスタイルを解決して(スタイルの計算を参照)、フレームを作成します。
WebKit では、スタイルを解決してレンダラを作成するプロセスを「アタッチメント」と呼びます。 すべての DOM ノードには「attach」メソッドがあります。アタッチは同期的であり、DOM ツリーにノードを挿入すると、新しいノードの「attach」メソッドが呼び出されます。
html タグと body タグを処理して、レンダリング ツリーのルートを構築します。ルート レンダリング オブジェクトは、CSS 仕様で「包含ブロック」と呼ばれるもの(他のすべてのブロックを含む最上位のブロック)に対応します。そのディメンションはビューポート、つまりブラウザ ウィンドウの表示領域のサイズです。このファイルは、Firefox では ViewPortFrame
、WebKit では RenderView
と呼ばれます。これは、ドキュメントが指すレンダリング オブジェクトです。ツリーの残りの部分は、DOM ノードの挿入として構築されます。
処理モデルに関する CSS2 仕様をご覧ください。
スタイル計算
レンダリング ツリーを作成するには、各レンダリング オブジェクトの視覚的なプロパティを計算する必要があります。そのためには、各要素のスタイル プロパティを計算します。
スタイルには、さまざまなオリジンのスタイルシート、インライン スタイル要素、HTML 内の視覚的プロパティ(「bgcolor」プロパティなど)が含まれます。後者は、対応する CSS スタイル プロパティに変換されます。
スタイルシートの生成元とは、ブラウザのデフォルトのスタイルシート、ページ作成者が提供するスタイルシート、ユーザーのスタイルシートです。これらはブラウザのユーザーが提供したスタイルシートです(ブラウザでは好みのスタイルを定義できます。たとえば Firefox では、「Firefox のプロファイル」フォルダにスタイルシートを配置します。
スタイルの計算には、次のような問題があります。
- スタイルデータは、多数のスタイル プロパティを保持する非常に大きな構造体であり、メモリの問題を引き起こす可能性があります。
各要素のマッチング ルールが最適化されていない場合、パフォーマンスの問題を引き起こす可能性があります。各要素のルールリスト全体を調べて一致を見つけるのは大変な作業です。セレクタは複雑な構造になっているため、一見期待どおりの経路でマッチング処理が始まらない場合がありますが、その経路は無意味であることが証明され、別の経路を試す必要があります。
たとえば、次のような複合セレクタがあります。
div div div div{ ... }
つまり、3 つの div の子孫である
<div>
にルールが適用されます。特定の<div>
要素にルールが適用されるかどうかを確認するとします。確認用にツリーの特定のパスを選択します。div が 2 つしかなく、ルールが適用されないことを確認するために、ノードツリーを上に移動する必要があるかもしれません。その後、ツリー内の別のパスを試す必要があります。ルールを適用するには、ルールの階層を定義する非常に複雑なカスケード ルールが必要です。
ブラウザがこれらの問題にどのように対処しているかを見てみましょう。
スタイルデータの共有
WebKit ノードは、スタイル オブジェクト(RenderStyle)を参照します。一部の条件では、これらのオブジェクトはノードによって共有できます。ノードは兄弟またはいとこであり、
- 各要素のマウス状態は同じである必要があります(たとえば、1 つを :hover に入れて、もう 1 つを :hover にすることはできません)。
- どちらの要素にも ID は指定できません
- タグ名は一致している必要があります
- クラス属性は一致させる必要があります
- マッピングされた属性のセットが同一である必要があります
- リンクの状態が一致している必要があります
- フォーカスの状態は一致している必要があります
- どちらの要素も属性セレクタの影響を受けません。影響を受けるとは、セレクタ内の任意の位置に属性セレクタを使用する任意のセレクタが一致すると定義されます。
- 要素にインライン スタイル属性を含めることはできません
- 兄弟セレクタはまったく使用しないでください。WebCore は、兄弟セレクタに遭遇すると単にグローバル スイッチをスローし、ドキュメント全体が存在する場合はスタイル共有を無効にします。これには、+ セレクタと、:first-child、:last-child などのセレクタが含まれます。
Firefox のルールツリー
Firefox には、スタイルの計算を容易にするために、ルールツリーとスタイル コンテキスト ツリーの 2 つのツリーがあります。 WebKit にもスタイル オブジェクトがありますが、スタイル コンテキスト ツリーのようにツリーに格納されるのではなく、関連するスタイルを指す DOM ノードだけです。
スタイル コンテキストには終了値が含まれます。値は、すべてのマッチング ルールを正しい順序で適用し、それらを論理値から具体的な値に変換する操作を行うことで計算されます。たとえば、論理値が画面に対するパーセンテージである場合は、論理値が計算され、絶対単位に変換されます。 ルールツリーの考え方はとても巧妙です。これにより、ノード間でこれらの値を共有して、再度計算する必要がなくなります。容量も節約できます
一致したルールはすべてツリーに格納されます。パスの下位のノードほど優先度が高くなります。 このツリーには、検出されたルールに一致するすべてのパスが含まれます。ルールの保存はゆっくりと行われます。すべてのノードの開始時にツリーが計算されるわけではありませんが、ノードのスタイルを計算する必要がある場合は常に、計算されたパスがツリーに追加されます。
これは、ツリーパスを辞書内の単語として見ることです。このルールツリーをすでに計算したとします。
コンテンツ ツリー内の別の要素のルールをマッチングする必要がある場合、一致したルールが(正しい順序で)B-E-I であることがわかるとします。パス A-B-E-I-L をすでに計算しているため、このパスはすでにツリー内にあります。やることが減ります。
ツリーによって作業がどのように節約されるかを見てみましょう。
構造体への分割
スタイル コンテキストは構造体に分割されます。これらの構造体には、枠線や色などの特定のカテゴリのスタイル情報が含まれています。構造体のすべてのプロパティは、継承されるか、継承されません。継承されるプロパティは、要素で定義されていない限り、親から継承されるプロパティです。継承されないプロパティ(「リセット」プロパティと呼ばれる)では、定義されていない場合はデフォルト値が使用されます。
ツリーは、構造体全体(計算された終了値を含む)をツリー内にキャッシュする場合に便利です。ボトムノードで構造体の定義が提供されていない場合、上位ノードにキャッシュされた構造体を使用できます。
ルールツリーを使ってスタイル コンテキストを計算する
特定の要素のスタイル コンテキストを計算するときは、まずルールツリー内のパスを計算するか、既存のパスを使用します。次に、新しいスタイル コンテキストの構造体を埋めるために、パスにルールの適用を開始します。パスの一番下のノード、つまり最も優先順位が高いノード(通常は最も狭い範囲のセレクタ)から開始し、構造体がいっぱいになるまでツリーを走査します。 ルールのノードに構造体の指定がない場合は、構造体を完全に指定してそれを指しているノードが見つかるまでツリーの上位に移動します。これは最適な最適化方法であり、構造体全体が共有されます。 これにより、終了値の計算とメモリを節約できます。
部分的な定義が見つかった場合は、構造体が完成するまでツリーの上位に移動します。
構造体の定義が見つからない場合は、構造体が「継承」型の場合は、コンテキスト ツリーで親の構造体を参照します。このケースでは、構造体の共有にも成功しています。リセット構造体の場合は、デフォルト値が使用されます。
最も具体的なノードで値が追加される場合は、それを実際の値に変換するために追加の計算を行う必要があります。その後、結果をツリーノードにキャッシュして、子が使用できるようにします。
要素に同じツリーノードを指す兄弟がある場合、スタイル コンテキスト全体をそれらの間で共有できます。
例を見てみましょう この HTML が
<html>
<body>
<div class="err" id="div1">
<p>
this is a <span class="big"> big error </span>
this is also a
<span class="big"> very big error</span> error
</p>
</div>
<div class="err" id="div2">another error</div>
</body>
</html>
次のルールが適用されます。
div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
わかりやすくするために、color 構造体と margin 構造体の 2 つの構造体のみを記入する必要があるとします。 color 構造体には 1 つのメンバー(color)のみが含まれます。 margin 構造体には 4 つの辺が含まれます。
作成されるルールツリーは次のようになります(ノード名(ノードが指すルールの番号)でノードがマークされます)。
コンテキスト ツリーは次のようになります(ノード名: それらが指すルールノード)。
HTML を解析し、2 番目の <div>
タグに到達したとします。このノードのスタイル コンテキストを作成し、スタイル構造体を埋める必要があります。
ルールを照合して、<div>
の一致ルールが 1、2、6 であることを確認します。つまり、要素を使用できる既存のパスがツリー内に存在しているため、ルール 6 用に別のノードを追加するだけで済みます(ルールツリーのノード F)。
スタイル コンテキストを作成して、コンテキスト ツリーに配置します。新しいスタイル コンテキストは、ルールツリーのノード F をポイントします。
次に、スタイル構造体を埋める必要があります。まず、margin 構造体の入力から始めます。最後のルールノード(F)は margin 構造体に追加されないため、前のノード挿入で計算されたキャッシュされた構造体が見つかるまでツリーの上位に移動し、それを使用できます。 それは、マージン ルールを指定した最上位のノードであるノード B にあります。
color 構造体の定義は存在するため、キャッシュされた構造体は使用できません。色には 1 つの属性があるため、他の属性を埋めるためにツリーの最上部に移動する必要はありません。終了値を計算し(文字列を RGB に変換するなど)、計算された構造体をこのノードにキャッシュします。
2 番目の <span>
要素の処理はさらに簡単になります。ルールを照合し、前のスパンと同様に、ルール G をポイントしていることになります。兄弟が同じノードを指しているため、スタイル コンテキスト全体を共有して、前のスパンのコンテキストをポイントするだけで済みます。
親から継承されたルールを含む構造体の場合、キャッシュはコンテキスト ツリーで実行されます(color プロパティは実際には継承されますが、Firefox ではリセットとして扱われ、ルールツリーにキャッシュされます)。
たとえば、段落内のフォントに関するルールを追加すると、次のようになります。
p {font-family: Verdana; font size: 10px; font-weight: bold}
この場合、コンテキスト ツリーの div の子である段落要素は、親と同じフォント構造体を共有できます。これは、その段落のフォント規則が指定されていない場合です。
ルールツリーのない WebKit では、一致した宣言が 4 回走査されます。まず、重要でない高優先度プロパティが適用されます(他のプロパティはそれらに依存するため、最初に適用する必要があるプロパティです。ディスプレイなど)。次に、優先度の高いプロパティ、通常の優先度で重要でないルール、通常の優先度の重要なルールが適用されます。 つまり、複数回出現するプロパティは、正しいカスケード順序に従って解決されます。最後の勝利。
まとめると、スタイル オブジェクト(その全体または内部の構造体の一部)を共有すると、問題 1 と 3 が解決します。Firefox のルールツリーも、プロパティを正しい順序で適用するのに役立ちます。
一致しやすくするためのルールの操作
スタイルルールには複数のソースがあります。
- CSS ルール(外部のスタイルシートまたはスタイル要素内)。
css p {color: blue}
- インライン スタイル属性(
html <p style="color: blue" />
など) - HTML 視覚属性(関連するスタイルルールにマッピングされます)
html <p bgcolor="blue" />
最後の 2 つはスタイル属性を所有しており、HTML 属性をキーとして使用してマッピングできるため、最後の 2 つは簡単に要素と照合できます。
前述の問題 2 で述べたように、CSS ルールのマッチングは難しい場合があります。この難易度を解決するために、アクセスしやすくするためにルールが操作されます。
スタイルシートを解析した後、セレクタに従って、複数のハッシュマップのうちの 1 つにルールが追加されます。ID、クラス名、タグ名別のマップと、これらのカテゴリに当てはまらないものすべてに対応する一般的なマップがあります。セレクタが id の場合、ルールは ID マップに追加されます。クラスの場合は、クラスマップに追加されます。
この操作により、ルールの照合がはるかに容易になります。すべての宣言を調べる必要はありません。要素に関連するルールをマップから抽出できます。この最適化により、ルールの 95% 以上が除外されるため、マッチング処理で考慮する必要すらなくなります(4.1)。
次のスタイルルールの例を見てみましょう。
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
最初のルールはクラスマップに挿入されます。2 つ目は ID マップ、3 つ目はタグマップです。
次の HTML フラグメントの場合、
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
まず、p 要素のルールを見つけます。クラスマップには「error」キーが含まれ、「p.error」のルールが表示されます。 div 要素には、id マップ(キーは id)とタグマップに関連するルールがあります。 したがって、残る作業は、キーによって抽出されたルールのうち、実際に一致するものを見つけることだけです。
たとえば、次のように div のルールがあるとします。
table div {margin: 5px}
キーは右端のセレクタであるため、引き続きタグマップから抽出されますが、テーブルの祖先を持たない div 要素とは一致しません。
WebKit と Firefox のどちらもこの操作を行います。
スタイルシートのカスケード順序
スタイル オブジェクトには、各視覚的属性(すべての CSS 属性だが、より一般的な属性)に対応するプロパティがあります。一致したルールのいずれでもプロパティが定義されていない場合、一部のプロパティは親要素のスタイル オブジェクトに継承されます。他のプロパティにはデフォルト値があります。
問題は複数の定義があるところから始まります。つまり、問題を解決するためのカスケード順序です。
スタイル プロパティの宣言は、複数のスタイルシートに記述できます。また、スタイルシート内で複数回出現することもできます。つまり、ルールを適用する順序が非常に重要になります。これは「カスケード順序」と呼ばれます。 CSS2 の仕様によると、カスケード順序は次のとおりです(昇順)。
- ブラウザの宣言
- ユーザーの通常宣言
- 作成者の通常の宣言
- 作成者の重要な宣言
- ユーザーの重要な宣言
ブラウザの宣言は重要度が最も低く、宣言が重要であるとマークされている場合にのみ、ユーザーは作成者をオーバーライドします。 同じ順序の宣言は、特異性、次に指定された順序で並べ替えられます。HTML のビジュアル属性は、一致する CSS 宣言に変換されます。優先度の低い作成者ルールとして扱われます。
詳細度
セレクタの限定性は、CSS2 仕様で次のように定義されています。
- セレクタ付きのルールではなく、「style」属性の場合は 1、それ以外の場合は 0(= a)とカウントします。
- セレクタ内の ID 属性の数を数える(= b)
- セレクタ内の他の属性と疑似クラスの数をカウントする(= c)
- セレクタ内の要素名と擬似要素の数をカウントする(= d)
4 つの数 a、b、c、d を(大きな底数体系で)連結すると、特異性が得られます。
使用する必要のある数値ベースは、いずれかのカテゴリの最大カウント数によって定義されます。
たとえば、a=14 の場合は 16 進数を使用できます。万が一 a=17 というようなまれなケースでは、17 桁の桁数が必要になります。 後者の状況は「html body div div p...」(セレクタ内に 17 個のタグがあり、可能性はあまりありません)のようなセレクタで発生します。
たとえば次のような例が考えられます。
* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
ルールの並べ替え
ルールは、一致すると、カスケード ルールに従って並べ替えられます。WebKit は、小さなリストにはバブル並べ替えを使用し、大きなリストにはマージ並べ替えを使用します。WebKit は、ルールの >
演算子をオーバーライドすることで並べ替えを実装します。
static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
int spec1 = r1.selector()->specificity();
int spec2 = r2.selector()->specificity();
return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
}
段階的なプロセス
WebKit は、すべての最上位のスタイルシート(@imports を含む)が読み込まれているかどうかをマークするフラグを使用します。添付時にスタイルが完全に読み込まれない場合は、プレースホルダが使用され、ドキュメント内でマークが付けられます。プレースホルダは、スタイルシートが読み込まれると再計算されます。
レイアウト
レンダラを作成してツリーに追加する際、位置とサイズは指定されません。これらの値を計算することをレイアウトまたはリフローと呼びます。
HTML はフローベースのレイアウト モデルを使用しているため、ほとんどの時間で 1 回のパスでジオメトリを計算できます。通常、「フロー中」の後方にある要素は、それ以前に「フロー中」にある要素のジオメトリには影響しないため、レイアウトはドキュメント内で左から右、上から下に進むことができます。ただし、例外として、HTML テーブルでは複数のパスが必要になる場合があります。
座標系はルートフレームを基準とします。上と左の座標が使用されます。
レイアウトは再帰プロセスです。HTML ドキュメントの <html>
要素に対応するルート レンダラから始まります。レイアウトはフレーム階層の一部または全部を再帰的に処理し、それを必要とするレンダラごとにジオメトリ情報を計算します。
ルート レンダラの位置は 0,0 で、その寸法はビューポート(ブラウザ ウィンドウの表示部分)です。
すべてのレンダラには「layout」または「reflow」メソッドがあり、各レンダラはレイアウトを必要とする子の layout メソッドを呼び出します。
ダーティ ビット システム
小さな変更ごとに完全なレイアウトを行わないように、ブラウザは「ダーティビット」システムを使用します。変更または追加されたレンダラは、自身と子を「ダーティ」としてマークし、レイアウトが必要。
「dirty」と「children are dirty」の 2 つのフラグがあります。これは、レンダラ自体は問題ないとしても、レイアウトを必要とする子が少なくとも 1 つあることを意味します。
グローバル レイアウトと増分レイアウト
レイアウトはレンダリング ツリー全体でトリガーできます。これは「グローバル」レイアウトです。考えられる原因は次のとおりです。
- フォントサイズの変更など、すべてのレンダラに影響するグローバルなスタイル変更。
- 画面のサイズが変更されたとき
レイアウトは増分にできます。ダーティなレンダラのみがレイアウトされます(これにより損傷が生じる可能性があり、追加のレイアウトが必要になります)。
インクリメンタル レイアウトは、レンダラがダーティな場合(非同期で)トリガーされます。たとえば、追加コンテンツがネットワークから送られて DOM ツリーに追加された後で、新しいレンダラがレンダー ツリーに追加される場合などです。
非同期レイアウトと同期レイアウト
増分レイアウトは非同期で実行されます。Firefox は、増分レイアウトの「リフロー コマンド」をキューに入れ、スケジューラがこれらのコマンドのバッチ実行をトリガーします。 WebKit には、増分レイアウトを実行するタイマーもあります。ツリーが走査され、「ダーティ」なレンダラがレイアウトアウトされます。
「offsetHeight」などのスタイル情報を要求するスクリプトは、増分レイアウトを同期的にトリガーできます。
通常、グローバル レイアウトは同期的にトリガーされます。
スクロール位置などの一部の属性が変更されたため、初期レイアウトの後にコールバックとしてレイアウトがトリガーされることがあります。
最適化
「サイズ変更」またはレンダラの位置(サイズではなく)の変更によってレイアウトがトリガーされると、レンダリング サイズはキャッシュから取得され、再計算はされません。
サブツリーのみが変更され、レイアウトがルートから開始しない場合があります。これは、テキスト フィールドに挿入されたテキストなど、変更がローカルで発生し、周囲に影響を与えない場合に発生することがあります(そうしないと、キー入力のたびにルートからレイアウトがトリガーされます)。
レイアウト プロセス
通常、レイアウトは次のパターンになります。
- 幅は親レンダラが決定します。
- 親は子の上に移動し、以下を行います。
- 子レンダラを配置します(x と y を設定します)。
- 必要に応じて子レイアウトを呼び出します(要素がダーティであるか、グローバル レイアウトであるかなど)。これにより、子の高さが計算されます。
- 親は、子の累積の高さ、マージンの高さ、パディングを使用して独自の高さを設定します。これは親レンダラの親によって使用されます。
- ダーティビットを false に設定します。
Firefox は、レイアウト(「reflow」)のパラメータとして「state」オブジェクト(nsHTMLReflowState)を使用します。特に、状態には親の幅が含まれます。
Firefox レイアウトの出力は「指標」オブジェクト(nsHTMLReflowMetrics)です。これには、レンダラで計算された高さが含まれます。
幅の計算
レンダラの幅は、コンテナ ブロックの幅、レンダラのスタイルの「width」プロパティ、マージン、枠線を使用して計算されます。
たとえば、次の div の幅は、
<div style="width: 30%"/>
WebKit では、次のように計算されます(RenderBox クラスのメソッド calcWidth)。
- コンテナの幅は、コンテナの availableWidth と 0 の最大値です。 この場合、availableWidth は contentWidth で、次のように計算されます。
clientWidth() - paddingLeft() - paddingRight()
clientWidth と clientHeight は、枠線とスクロールバーを除くオブジェクトの内部を表します。
要素の幅は「width」スタイル属性です。 コンテナの幅に対するパーセンテージを計算することで、絶対値として計算されます。
水平方向の枠線とパディングが追加されました。
ここまでは「優先幅」の計算でしたが、これで、最小幅と最大幅が計算されます。
優先する幅が最大幅より大きい場合は、最大幅が使用されます。値が最小幅(分割できない最小単位)より小さい場合は、最小幅が使用されます。
値は、レイアウトが必要な場合にキャッシュに保存されますが、幅は変更されません。
改行
レイアウトの途中でレンダラが中断する必要があると判断した場合、レンダラは停止し、レイアウトの親に中断が必要であることを伝播します。親は追加のレンダラを作成し、そのレンダラでレイアウトを呼び出します。
絵画
ペイント ステージでは、レンダー ツリーが走査され、レンダラの paint() メソッドが呼び出されて画面にコンテンツが表示されます。 ペイントでは UI インフラストラクチャ コンポーネントを使用します。
グローバルおよび増分
レイアウトと同様に、ペイントはグローバルにすることも(ツリー全体がペイントされる)ことも、インクリメンタルにすることもできます。増分ペイントでは、一部のレンダラがツリー全体に影響を与えない方法で変更されます。変更されたレンダラは、画面上の長方形を無効にします。その結果、OS はそれを「ダーティな領域」と見なし、「ペイント」イベントを生成します。 OS はこの処理を巧みに行い、複数のリージョンを 1 つに統合します。Chrome では、レンダラがメインプロセスとは異なるプロセスにあるため、より複雑になります。Chrome は、OS の動作をある程度シミュレートします。 プレゼンテーションはこれらのイベントをリッスンし、メッセージをレンダリング ルートに委任します。関連するレンダラに達するまで、ツリーが走査されます。アプリは自身(通常はその子)を再描画します。
描画順序
CSS2 では、描画処理の順序を定義しています。これは実際には、スタッキング コンテキストで要素がスタックされる順序です。スタックは背面から前面にペイントされるため、この順序はペイントに影響します。ブロック レンダラのスタック順序は次のとおりです。
- 背景色
- 背景画像
- border
- 子供
- アウトライン
Firefox のディスプレイ リスト
Firefox はレンダー ツリーを調べて、ペイントされた長方形のディスプレイ リストを作成します。これには、長方形に関連するレンダラが適切なペイント順序(レンダラの背景、枠線など)で含まれています。
これにより、再ペイントのためにツリーを 1 回だけ走査するだけで済みます。つまり、すべての背景をペイントした後、すべての画像をペイントしてから、すべての境界をペイントします。
Firefox は、隠される要素(他の不透明な要素の真下に要素が完全に隠れるなど)を追加しないようにすることで、プロセスを最適化します。
WebKit レクタングル ストレージ
再描画の前に、WebKit は古い長方形をビットマップとして保存します。次に、新しい長方形と古い長方形の差分のみを描画します。
動的な変更
ブラウザは、変更に応じて可能な限り最小限のアクションを試行します。そのため、要素の色を変更しても、要素は再描画されるだけです。要素の位置を変更すると、要素、その子、場合によっては兄弟のレイアウトと再描画が発生します。DOM ノードを追加すると、ノードのレイアウトと再描画が発生します。「html」要素のフォントサイズを大きくするなどの大幅な変更を行うと、キャッシュの無効化、ツリー全体の再レイアウト、再描画が発生します。
レンダリング エンジンのスレッド
レンダリング エンジンはシングル スレッドです。ネットワーク オペレーションを除き、ほぼすべてが 1 つのスレッドで行われます。Firefox と Safari では、これはブラウザのメインスレッドです。Chrome ではタブプロセスのメインスレッドです。
ネットワーク操作は複数の並列スレッドで実行できます。並列接続の数には制限があります(通常 2 ~ 6 接続)。
イベントループ
ブラウザのメインスレッドはイベントループです。これは、プロセスを維持するための無限ループです。イベント(レイアウト イベントやペイント イベントなど)を待機して処理します。Firefox でメイン イベント ループのコードは次のようになります。
while (!mExiting)
NS_ProcessNextEvent(thread);
CSS2 ビジュアル モデル
キャンバス
CSS2 仕様によると、キャンバスという用語は、ブラウザがコンテンツを描画する「フォーマット構造がレンダリングされるスペース」を指します。
キャンバスは、スペースの各寸法に対して無限にありますが、ブラウザの初期の幅はビューポートの寸法に基づいて選択されます。
www.w3.org/TR/CSS2/zindex.html によると、キャンバスが別のキャンバスに含まれている場合は透明になり、そうでない場合はブラウザで色が指定されます。
CSS Box モデル
CSS ボックスモデルは、ドキュメント ツリー内の要素に対して生成され、視覚的な書式設定モデルに従ってレイアウトされる長方形のボックスを記述します。
各ボックスには、コンテンツ領域(テキスト、画像など)と、必要に応じて周囲のパディング、境界、マージン領域があります。
各ノードが 0 ~ n 個のボックスを生成します。
すべての要素には、生成されるボックスの種類を決定する「display」プロパティがあります。
例:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
デフォルトはインラインですが、ブラウザのスタイルシートで他のデフォルトが設定されていることもあります。たとえば、「div」要素のデフォルト表示は「block」です。
デフォルトのスタイルシートのサンプルについては、www.w3.org/TR/CSS2/sample.html をご覧ください。
ポジショニング方法
次の 3 つのスキームがあります。
- 標準: オブジェクトはドキュメント内の位置に従って配置されます。つまり、レンダリング ツリー内の位置は DOM ツリー内の位置と同様に、ボックスの種類と寸法に従ってレイアウトされます。
- フローティング: オブジェクトはまず通常の流れのように配置され、その後、できるだけ左または右に移動します。
- 絶対: オブジェクトは、レンダリング ツリーの DOM ツリーとは異なる場所に配置されます
配置方法は、「position」プロパティと「float」属性で設定します。
- 通常のフローを引き起こし、
- 絶対位置と固定により
静的配置では、位置は定義されず、デフォルトの配置が使用されます。 他のスキームでは、作成者が位置を指定(上、下、左、右)します。
ボックスのレイアウトは、以下によって決まります。
- ボックスの種類
- 箱の寸法
- ポジショニング方法
- 画像サイズや画面サイズなどの外部情報
ボックスの種類
ブロック ボックス: 1 つのブロックを形成し、ブラウザ ウィンドウ内に独自の長方形を表示します。
インライン ボックス: 独自のブロックはありませんが、含まれるブロックの内側にあります。
ブロックは垂直方向に 1 つずつフォーマットされます。インラインは水平方向に書式設定されます。
インライン ボックスは、行または「ライン ボックス」の内側に配置します。線の高さは、最も背の高いボックスと同程度ですが、ボックスが「ベースライン」に揃えられると、もっと大きくなることがあります。つまり、要素の底部が、底部以外の別のボックスのポイントに揃えられます。 コンテナの幅が十分でない場合、インラインは複数行に配置されます。 これが通常、段落内で起こります。
位置付け
相対的
相対位置 - 通常どおりに配置してから、必要な差分だけ移動します。
浮動小数点
フロート ボックスが線の左または右にシフトする。興味深い特徴は、他のボックスが周囲を流れていることです。HTML:
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
次のようになります。
絶対的および固定的
レイアウトは、通常のフローに関係なく厳密に定義されます。要素は通常のフローに参加しません。ディメンションはコンテナを基準とします。 固定の場合、コンテナはビューポートです。
レイヤ表現
これは、Z-Index の CSS プロパティで指定されます。 これは、ボックスの 3 次元、つまり「z 軸」に沿った位置を表します。
ボックスはスタック(スタッキング コンテキストと呼ばれる)に分割されます。各スタックでは、後ろの要素が最初に描画され、その上に前方の要素がユーザーの近くで描画されます。重なり合う場合、先頭の要素で前の要素が非表示になります。
スタックは Z-Index プロパティに従って並べ替えられます。 「Z-Index」プロパティを持つボックスはローカル スタックを形成します。 ビューポートには外側のスタックがあります。
例:
<style type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</style>
<p>
<div
style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
</div>
<div
style="z-index: 1;background-color:green;width: 2in; height: 2in;">
</div>
</p>
結果は次のようになります。
赤い div はマークアップ内の緑の div より前にあり、通常のフローでは描画前に描画されていましたが、Z-Index プロパティの方が高いため、ルートボックスで保持されるスタック内では前方になります。
リソース
ブラウザ アーキテクチャ
- Greenkurth、Alanウェブブラウザのリファレンス アーキテクチャ(PDF)
- Gupta、Vineet、ブラウザの仕組み - パート 1 - アーキテクチャ
解析
- Aho、Sethi、Ullman、『Compilers: Principles, Techniques, and Tools』(別名「Dragonbook」)、Addison-Wesley、1986 年
- Rick Jelliffe です。The Bold and the Beautiful: HTML 5 の新しい 2 つのドラフト
Firefox
- L.David Baron、Faster HTML and CSS: Layout Engine Internals for Web Developers
- L.David Baron、Faster HTML and CSS: Layout Engine Internals for Web Developers(Google テクニカル トークの動画)
- L.David Baron、Mozilla's Layout Engine
- L.David Baron、Mozilla Style System Documentation
- Chris Waterson、Notes on HTML Reflow
- Chris Waterson、Gecko の概要
- Alexander Larsson、The life of an HTML HTTP request
WebKit
- David Hyatt、CSS の実装(パート 1)
- David Hyatt、An Overview of WebCore
- David Hyatt、WebCore Rendering
- David Hyatt、The FOUC Problem
W3C の仕様
ブラウザのビルド手順
翻訳
このページは日本語に 2 回翻訳されています。
- How Browsers Work - Behind the Scenes of Modern Web Browsers(ja)(著者: @kosei)
- ブラウザってどうやって動いてるの?(モダン WEB ブラウザ シーンの裏側(投稿者: @ikeike443、@kiyoto01)。
韓国語とトルコ語の外部でホストされている翻訳を表示できます。
みなさんもおつかれさまでした。