ブラウザの仕組み

最新のウェブブラウザの舞台裏

序文

WebKit と Gecko の内部オペレーションに関するこの包括的な入門書は、イスラエルのデベロッパーである Tali Garsiel 氏による多くの研究の成果です。数年かけて、ブラウザの内部に関する公開データをすべて確認し、ウェブブラウザのソースコードを読み込むのに多くの時間を費やしました。彼女は次のように書いています。

ウェブ デベロッパーにとって、ブラウザの内部動作を学ぶことは、より適切な意思決定を行い、開発のベスト プラクティスの根拠を理解するのに役立ちます。このドキュメントはかなり長いですが、時間をかけてじっくり読むことをおすすめします。やって良かったと思えるはずです。

Paul Irish、Chrome デベロッパー リレーションズ

はじめに

ウェブブラウザは、最も広く使用されているソフトウェアです。この入門書では、それらが舞台裏でどのように機能するかについて説明します。アドレスバーに「google.com」と入力してから、ブラウザ画面に Google ページが表示されるまでの流れを見ていきます。

今回取り上げるブラウザ

現在、パソコンで主に使用されているブラウザは、Chrome、Internet Explorer、Firefox、Safari、Opera の 5 つです。モバイルの主なブラウザは、Android ブラウザ、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(ワールド ワイド ウェブ コンソーシアム)によって管理されています。長年にわたり、ブラウザは仕様の一部にのみ準拠し、独自の拡張機能を開発してきました。このため、ウェブ作成者にとって深刻な互換性の問題が発生していました。現在、ほとんどのブラウザは多かれ少なかれ仕様に準拠しています。

ブラウザのユーザー インターフェースには、多くの共通点があります。一般的なユーザー インターフェース要素には、次のようなものがあります。

  1. URI を挿入するためのアドレスバー
  2. [戻る] ボタンと [進む] ボタン
  3. ブックマーク オプション
  4. 現在のドキュメントの読み込みを更新または停止するための更新ボタンと停止ボタン
  5. ホームページに移動するホームボタン

奇妙なことに、ブラウザのユーザー インターフェースは正式な仕様で指定されているわけではなく、長年の経験とブラウザ同士の模倣によって形成された優れたプラクティスから生まれたものです。HTML5 仕様では、ブラウザに必要な UI 要素は定義されていませんが、一般的な要素がいくつかリストされています。たとえば、アドレスバー、ステータスバー、ツールバーなどです。もちろん、Firefox のダウンロード マネージャーなど、特定のブラウザに固有の機能もあります。

上位インフラストラクチャ

ブラウザの主なコンポーネントは次のとおりです。

  1. ユーザー インターフェース: アドレスバー、戻る / 進むボタン、ブックマーク メニューなど。リクエストされたページが表示されるウィンドウを除く、ブラウザ表示のすべての部分。
  2. ブラウザ エンジン: UI とレンダリング エンジンの間でアクションをマーシャリングします。
  3. レンダリング エンジン: リクエストされたコンテンツの表示を担当します。たとえば、リクエストされたコンテンツが HTML の場合、レンダリング エンジンは HTML と CSS を解析し、解析されたコンテンツを画面に表示します。
  4. ネットワーキング: HTTP リクエストなどのネットワーク呼び出し。プラットフォームに依存しないインターフェースの背後で、プラットフォームごとに異なる実装を使用します。
  5. UI バックエンド: コンボボックスやウィンドウなどの基本的なウィジェットの描画に使用されます。このバックエンドは、プラットフォーム固有ではない汎用インターフェースを公開します。内部では、オペレーティング システムのユーザー インターフェース メソッドを使用します。
  6. JavaScript インタープリタ。JavaScript コードの解析と実行に使用されます。
  7. データ ストレージ。これは永続レイヤです。ブラウザは、Cookie などのあらゆる種類のデータをローカルに保存する必要がある場合があります。ブラウザは、localStorage、IndexedDB、WebSQL、FileSystem などのストレージ メカニズムもサポートしています。
ブラウザ コンポーネント
図 1: ブラウザのコンポーネント

Chrome などのブラウザは、レンダリング エンジンの複数のインスタンス(タブごとに 1 つ)を実行することに注意してください。各タブは個別のプロセスで実行されます。

レンダリング エンジン

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

デフォルトでは、レンダリング エンジンは HTML および XML のドキュメントと画像を表示できます。プラグインや拡張機能を使用して、他の種類のデータを表示することもできます。たとえば、PDF ビューア プラグインを使用して PDF ドキュメントを表示できます。ただし、この章では、主なユースケースである、CSS を使用してフォーマットされた HTML と画像の表示に焦点を当てます。

ブラウザによって使用するレンダリング エンジンは異なります。Internet Explorer は Trident、Firefox は Gecko、Safari は WebKit を使用します。Chrome と Opera(バージョン 15 以降)は、WebKit のフォークである Blink を使用しています。

WebKit は、Linux プラットフォーム用のエンジンとして開発が開始されたオープンソースのレンダリング エンジンです。その後、Apple によって Mac と Windows をサポートするように変更されました。

メインフロー

レンダリング エンジンは、リクエストされたドキュメントのコンテンツをネットワーク層から取得し始めます。通常、これは 8 kB のチャンクで行われます。

その後、レンダリング エンジンの基本的なフローは次のようになります。

レンダリング エンジンの基本フロー
図 2: レンダリング エンジンの基本的なフロー

レンダリング エンジンは、HTML ドキュメントの解析を開始し、要素を「コンテンツ ツリー」と呼ばれるツリー内の DOM ノードに変換します。エンジンは、外部 CSS ファイルとスタイル要素の両方のスタイルデータを解析します。スタイル設定情報と HTML の視覚的な指示を組み合わせて、別のツリー(レンダリング ツリー)が作成されます。

レンダリング ツリーには、色や寸法などの視覚属性を持つ長方形が含まれます。長方形は、画面に表示される正しい順序になっています。

レンダリング ツリーの構築後、「レイアウト」プロセスが実行されます。つまり、各ノードに画面に表示される正確な座標を指定します。次のステージはペイントです。レンダリング ツリーが走査され、UI バックエンド レイヤを使用して各ノードがペイントされます。

これは段階的なプロセスであることを理解することが重要です。ユーザー エクスペリエンスを向上させるため、レンダリング エンジンはできるだけ早く画面にコンテンツを表示しようとします。すべての HTML が解析されるまで待ってから、レンダリング ツリーの構築とレイアウトを開始するわけではありません。コンテンツの一部が解析されて表示される一方で、ネットワークから継続的に受信される残りのコンテンツの処理が続行されます。

メインフローの例

WebKit のメインフロー。
図 3: WebKit のメインフロー
Mozilla の Gecko レンダリング エンジンのメインフロー。
図 4: Mozilla の Gecko レンダリング エンジンのメインフロー

図 3 と図 4 からわかるように、WebKit と Gecko では使用する用語が若干異なりますが、フローは基本的に同じです。

Gecko では、視覚的にフォーマットされた要素のツリーを「フレームツリー」と呼びます。各要素はフレームです。WebKit では「レンダーツリー」という用語が使用され、これは「レンダー オブジェクト」で構成されます。WebKit では要素の配置に「レイアウト」という用語を使用しますが、Gecko では「リフロー」と呼びます。「アタッチメント」は、DOM ノードと視覚情報を接続してレンダリング ツリーを作成するための WebKit の用語です。意味論的でない小さな違いとして、Gecko には HTML と DOM ツリーの間に余分なレイヤがあります。これは「コンテンツ シンク」と呼ばれ、DOM 要素を作成するためのファクトリです。このフローの各部分について説明します。

解析 - 全般

解析はレンダリング エンジン内で非常に重要なプロセスであるため、もう少し詳しく説明します。 まず、解析について簡単に説明します。

ドキュメントを解析するとは、コードで使用できる構造に変換することを意味します。解析の結果は通常、ドキュメントの構造を表すノードのツリーになります。これは、解析ツリーまたは構文ツリーと呼ばれます。

たとえば、式 2 + 3 - 1 を解析すると、次のツリーが返されます。

数式ツリーノード。
図 5: 数式ツリーノード

文法

解析は、ドキュメントが従う構文ルール(記述された言語または形式)に基づいています。解析可能なすべての形式には、語彙と構文ルールで構成される決定論的な文法が必要です。これは文脈自由文法と呼ばれます。人間の言語はこのような言語ではないため、従来の解析手法では解析できません。

パーサーと字句解析ツールの組み合わせ

解析は、字句解析と構文解析の 2 つのサブプロセスに分けることができます。

字句解析は、入力をトークンに分割するプロセスです。トークンは言語の語彙、つまり有効な構成要素の集合です。人間の言語では、その言語の辞書に掲載されているすべての単語で構成されます。

構文解析とは、言語の構文規則を適用することです。

パーサーは通常、2 つのコンポーネント間で作業を分割します。1 つは、入力を有効なトークンに分割する役割を担うレクサー(トークナイザーと呼ばれることもあります)で、もう 1 つは、言語構文ルールに従ってドキュメント構造を分析して解析ツリーを構築する役割を担うパーサーです。

字句解析器は、空白文字や改行などの不要な文字を削除する方法を認識しています。

ソース ドキュメントから解析ツリーへ
図 6: ソース ドキュメントから解析ツリーへ

解析プロセスは反復的です。通常、パーサーは字句解析器に新しいトークンを要求し、そのトークンを構文規則のいずれかと照合しようとします。ルールが一致すると、トークンに対応するノードが解析ツリーに追加され、パーサーは別のトークンを要求します。

一致するルールがない場合、パーサーはトークンを内部に保存し、内部に保存されたすべてのトークンに一致するルールが見つかるまでトークンを要求し続けます。ルールが見つからない場合、パーサーは例外を発生させます。これは、ドキュメントが無効で、構文エラーが含まれていることを意味します。

Translation

多くの場合、解析ツリーは最終的なプロダクトではありません。解析は、入力ドキュメントを別の形式に変換する変換でよく使用されます。たとえば、コンパイルなどです。ソースコードをマシンコードにコンパイルするコンパイラは、まずソースコードを解析して変換し、解析ツリーに変換してから、そのツリーをマシンコード ドキュメントに変換します。

コンパイル フロー
図 7: コンパイル フロー

解析の例

図 5 では、数式から解析ツリーを構築しました。簡単な数学言語を定義して、解析プロセスを見てみましょう。

構文:

  1. 言語構文の構成要素は、式、項、演算です。
  2. 言語には任意の数の式を含めることができます。
  3. 式は、「項」の後に「演算」が続き、その後に別の項が続くものとして定義されます。
  4. 演算子はプラス トークンまたはマイナス トークン
  5. 項は、整数トークンまたは式です。

入力 2 + 3 - 1 を分析してみましょう。

ルールに一致する最初の部分文字列は 2 です。ルール 5 に従って、これは用語です。2 番目の一致は 2 + 3 です。これは 3 番目のルール(用語、演算、別の用語の順)に一致します。次のマッチは、入力の末尾でのみヒットします。2 + 3 - 1 は式です。2 + 3 が項であることはすでにわかっているため、項の後に演算が続き、その後に別の項が続くことになります。2 + + はどのルールにも一致しないため、無効な入力です。

語彙と構文の正式な定義

語彙は通常、正規表現で表されます。

たとえば、言語は次のように定義されます。

INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -

ご覧のとおり、整数は正規表現で定義されています。

構文は通常、BNF と呼ばれる形式で定義されます。言語は次のように定義されます。

expression :=  term  operation  term
operation :=  PLUS | MINUS
term := INTEGER | expression

文法が文脈自由文法であれば、言語は通常のパーサーで解析できると述べました。 文脈自由文法の直感的な定義は、BNF で完全に表現できる文法です。正式な定義については、Wikipedia のコンテキストフリー文法に関する記事をご覧ください。

パーサーの種類

パーサーには、トップダウン パーサーとボトムアップ パーサーの 2 種類があります。直感的に説明すると、トップダウン パーサーは構文の上位構造を調べて、ルールの一致を見つけようとします。ボトムアップ パーサーは、入力から開始し、低レベルのルールから高レベルのルールが満たされるまで、徐々に構文ルールに変換します。

2 種類のパーサーがこの例をどのように解析するかを見てみましょう。

トップダウン パーサーは、上位レベルのルールから開始します。2 + 3 を式として識別します。次に、2 + 3 - 1 を式として識別します(式を識別するプロセスは、他のルールと一致するように進化しますが、開始点は最上位のルールです)。

ボトムアップ パーサーは、ルールが一致するまで入力をスキャンします。一致する入力がルールに置き換えられます。この処理は、入力が終了するまで続きます。部分的に一致した式は、パーサーのスタックに配置されます。

スタック 入力
2 + 3 - 1
term + 3 - 1
項演算 3 - 1
式 - 1
式オペレーション 1
式 -

このタイプのボトムアップ パーサーは、シフト還元パーサーと呼ばれます。入力が右にシフトされ(ポインタが最初に入力の開始位置を指し、右に移動すると想像してください)、徐々に構文ルールに還元されるためです。

パーサーを自動的に生成する

パーサーを生成できるツールがあります。言語の文法(語彙と構文規則)をパーサー ジェネレータに渡すと、パーサー ジェネレータは動作するパーサーを生成します。パーサーを作成するには、解析に関する深い理解が必要です。最適化されたパーサーを手動で作成するのは容易ではないため、パーサー ジェネレータが非常に役立ちます。

WebKit は、2 つのよく知られたパーサ ジェネレータを使用します。字句解析器を作成するための Flex と、パーサを作成するための Bison です(Lex と Yacc という名前で遭遇することもあります)。Flex 入力は、トークンの正規表現定義を含むファイルです。Bison の入力は、BNF 形式の言語構文ルールです。

HTML パーサー

HTML パーサーの役割は、HTML マークアップを解析して変換し、解析ツリーにすることです。

HTML の文法

HTML の語彙と構文は、W3C 組織が作成した仕様で定義されています。

解析の概要で説明したように、文法構文は BNF などの形式を使用して正式に定義できます。

残念ながら、従来のパーサーのトピックはすべて HTML には適用されません(これらを取り上げたのは単に興味深いからではなく、CSS と JavaScript の解析で使用されるためです)。HTML は、パーサーが必要とするコンテキスト フリー文法では簡単に定義できません。

HTML を定義するための正式な形式(DTD(Document Type Definition))はありますが、これはコンテキスト フリー文法ではありません。

一見すると、これは奇妙に思えます。HTML は XML にかなり近いからです。利用可能な XML パーサーは多数あります。HTML の XML バリエーションである XHTML がありますが、大きな違いは何ですか?

違いは、HTML のアプローチの方が「寛容」であることです。特定のタグを省略でき(省略したタグは暗黙的に追加されます)、開始タグや終了タグを省略できることもあります。全体として、XML の厳格で要求の厳しい構文とは対照的に、「ソフト」な構文です。

この一見小さな違いが、大きな違いを生みます。これは、HTML が非常に人気がある主な理由です。間違いを許容し、ウェブ作成者の作業を容易にします。一方で、正式な文法を記述することが難しくなります。まとめると、HTML の文法はコンテキスト フリーではないため、従来のパーサーでは簡単に解析できません。HTML は XML パーサーで解析できません。

HTML DTD

HTML 定義は DTD 形式です。この形式は、SGML ファミリーの言語を定義するために使用されます。この形式には、許可されるすべての要素、その属性、階層の定義が含まれています。前述のように、HTML DTD はコンテキスト フリー文法を形成しません。

DTD にはいくつかのバリエーションがあります。厳格モードは仕様にのみ準拠していますが、他のモードでは、過去にブラウザで使用されていたマークアップがサポートされています。目的は、古いコンテンツとの下位互換性を確保することです。現在の厳格な 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 ツリーに変換されます。

サンプル マークアップの DOM ツリー
図 8: マークアップ例の 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 は通常のトップダウン パーサーやボトムアップ パーサーを使用して解析することはできません。

その理由は次のとおりです。

  1. 言語の寛容な性質。
  2. ブラウザには、無効な HTML のよく知られたケースをサポートするための従来のエラー耐性があるという事実。
  3. 解析プロセスはリエントラントです。他の言語では、解析中にソースが変更されることはありませんが、HTML では、動的コード(document.write() 呼び出しを含むスクリプト要素など)によって追加のトークンが追加される可能性があるため、解析プロセスで実際に入力が変更されます。

通常の解析手法を使用できないため、ブラウザは HTML を解析するためのカスタム パーサーを作成します。

解析アルゴリズムについては、HTML5 仕様で詳しく説明されています。 このアルゴリズムは、トークン化とツリー構築の 2 つのステージで構成されています。

トークン化は、入力をトークンに解析する字句解析です。HTML トークンには、開始タグ、終了タグ、属性名、属性値などがあります。

トークナイザーはトークンを認識してツリー コンストラクタに渡し、次の文字を消費して次のトークンを認識します。この処理を入力の最後まで繰り返します。

HTML 解析フロー(HTML5 仕様から引用)
図 9: HTML 解析フロー(HTML5 仕様から引用)

トークン化アルゴリズム

アルゴリズムの出力は 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> 入力は、前のケースと同様に扱われます。

入力例のトークン化
図 10: 入力例のトークン化

ツリー構築アルゴリズム

パーサーが作成されると、Document オブジェクトが作成されます。ツリー構築ステージでは、Document をルートとする DOM ツリーが変更され、要素が追加されます。トークナイザーによって出力された各ノードは、ツリー コンストラクタによって処理されます。 各トークンについて、仕様では、どの DOM 要素がトークンに関連し、このトークン用に作成されるかを定義します。要素が DOM ツリーと開いている要素のスタックに追加されます。このスタックは、ネストの不一致と閉じられていないタグを修正するために使用されます。このアルゴリズムは、ステートマシンとしても記述されます。この状態は「挿入モード」と呼ばれます。

入力例のツリー構築プロセスを見てみましょう。

<html>
  <body>
    Hello world
  </body>
</html>

ツリー構築ステージへの入力は、トークン化ステージからの一連のトークンです。1 つ目のモードは「初期モード」です。「html」トークンを受け取ると、「before html」モードに移行し、そのモードでトークンが再処理されます。これにより、HTMLHtmlElement 要素が作成され、ルート Document オブジェクトに追加されます。

状態が「before head」に変わります。次に、「body」トークンが受信されます。「head」トークンがなくても、HTMLHeadElement が暗黙的に作成され、ツリーに追加されます。

次に、「in head」モードに移行し、その後「after head」に移行します。body トークンが再処理され、HTMLBodyElement が作成されて挿入され、モードが "in body" に移行します。

「Hello world」という文字列の文字トークンが受信されました。最初の文字で「Text」ノードが作成されて挿入され、他の文字はそのノードに追加されます。

body 終了トークンを受信すると、"after body" モードに移行します。 ここで、HTML の終了タグを受け取ると、「after after body」モードに移行します。ファイルの末尾トークンを受信すると、解析が終了します。

HTML の例のツリー構造。
図 11: HTML の例のツリー構造

解析が完了したときのアクション

この段階で、ブラウザはドキュメントをインタラクティブとしてマークし、「遅延」モードのスクリプト(ドキュメントの解析後に実行する必要があるスクリプト)の解析を開始します。ドキュメントの状態が「完了」に設定され、「読み込み」イベントが発生します。

トークン化とツリー構築の完全なアルゴリズムは、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 ドキュメントを多く処理する必要があるため、パーサーはエラーを許容する必要があります。

少なくとも次のエラー条件を処理する必要があります。

  1. 追加される要素は、一部の外側のタグ内で明示的に禁止されています。この場合、要素を禁止するタグまですべてのタグを閉じ、その後に追加する必要があります。
  2. 要素を直接追加することはできません。ドキュメントの作成者が、間にタグを挿入し忘れた可能性があります(または、間のタグがオプションである可能性があります)。これは、HTML、HEAD、BODY、TBODY、TR、TD、LI などのタグに当てはまります(他にもあるかもしれません)。
  3. インライン要素内にブロック要素を追加します。次の上位ブロック要素までのすべてのインライン要素を閉じます。
  4. それでも問題が解決しない場合は、要素を追加できるようになるまで要素を閉じるか、タグを無視してください。

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 は空白文字を表します)。ルールセットには中かっこが含まれ、その中に宣言が記述されます。必要に応じて、セミコロンで区切って複数の宣言を記述することもできます。 「declaration」と「selector」は、以下の BNF 定義で定義されます。

WebKit CSS パーサー

WebKit は、Flex と Bison のパーサー ジェネレータを使用して、CSS 文法ファイルからパーサーを自動的に作成します。パーサーの概要で説明したように、Bison はボトムアップのシフト還元パーサーを作成します。Firefox は、手動で作成されたトップダウン パーサーを使用します。どちらの場合も、各 CSS ファイルは StyleSheet オブジェクトに解析されます。各オブジェクトには CSS ルールが含まれています。CSS ルール オブジェクトには、セレクタ オブジェクト、宣言オブジェクト、CSS 文法に対応するその他のオブジェクトが含まれます。

CSS を解析しています。
図 12: 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 ボックスに対応する長方形の領域を表します。これには、幅、高さ、位置などの幾何学的情報が含まれます。

ボックスタイプは、ノードに関連するスタイル属性の「display」値の影響を受けます(スタイル計算のセクションを参照)。以下は、display 属性に応じて DOM ノードに作成するレンダラのタイプを決定する WebKit コードです。

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 つの例は、壊れた HTML です。CSS 仕様によると、インライン要素にはブロック要素のみ、またはインライン要素のみを含める必要があります。混合コンテンツの場合、インライン要素をラップするために匿名ブロック レンダラが作成されます。

一部のレンダリング オブジェクトは DOM ノードに対応していますが、ツリー内の同じ場所にはありません。浮動要素と絶対位置要素はフロー外にあり、ツリーの別の部分に配置され、実際のフレームにマッピングされます。プレースホルダ フレームは、それらが配置されるべき場所です。

レンダリング ツリーと対応する DOM ツリー。
図 13: レンダリング ツリーと対応する DOM ツリー。「ビューポート」は、最初のコンテナ ブロックです。WebKit では「RenderView」オブジェクトになります。

ツリーを構築するフロー

Firefox では、プレゼンテーションは DOM 更新のリスナーとして登録されます。 プレゼンテーションはフレームの作成を FrameConstructor に委任し、コンストラクタはスタイルを解決して(スタイルの計算を参照)、フレームを作成します。

WebKit では、スタイルを解決してレンダラを作成するプロセスを「アタッチメント」と呼びます。すべての DOM ノードに「attach」メソッドがあります。アタッチは同期的で、DOM ツリーへのノードの挿入により、新しいノードの「attach」メソッドが呼び出されます。

html タグと body タグを処理すると、レンダリング ツリーのルートが構築されます。ルート レンダリング オブジェクトは、CSS 仕様で包含ブロックと呼ばれるもの(他のすべてのブロックを含む最上位のブロック)に対応します。そのサイズはビューポート(ブラウザ ウィンドウの表示領域のサイズ)です。Firefox では ViewPortFrame、WebKit では RenderView と呼ばれています。これは、ドキュメントが参照するレンダリング オブジェクトです。ツリーの残りの部分は、DOM ノードの挿入として構築されます。

処理モデルに関する CSS2 仕様をご覧ください。

スタイルの計算

レンダリング ツリーを構築するには、各レンダリング オブジェクトの視覚的プロパティを計算する必要があります。これは、各要素のスタイル プロパティを計算することで行われます。

スタイルには、さまざまなソースのスタイルシート、インライン スタイル要素、HTML の視覚プロパティ(「bgcolor」プロパティなど)が含まれます。後者は、一致する CSS スタイル プロパティに変換されます。

スタイルシートのオリジンは、ブラウザのデフォルトのスタイルシート、ページ作成者が提供するスタイルシート、ユーザー スタイルシートです。これらは、ブラウザ ユーザーが提供するスタイルシートです(ブラウザでは、お気に入りのスタイルを定義できます。たとえば、Firefox では、スタイルシートを「Firefox Profile」フォルダに配置することで、この処理を行います。

スタイルの計算には、いくつかの困難が伴います。

  1. スタイルデータは非常に大きな構造で、多数のスタイル プロパティを保持しているため、メモリの問題を引き起こす可能性があります。
  2. 各要素に一致するルールを見つける処理が最適化されていないと、パフォーマンスの問題が発生する可能性があります。各要素の一致を見つけるためにルールリスト全体を走査するのは、負荷の高いタスクです。セレクタは複雑な構造を持つことがあり、その場合、一見有望なパスで一致プロセスが開始されたものの、そのパスは無駄であることが判明し、別のパスを試す必要が生じることがあります。

    たとえば、次の複合セレクタがあるとします。

    div div div div{
    ...
    }
    

    ルールが 3 つの div の子孫である <div> に適用されることを意味します。特定の <div> 要素にルールが適用されるかどうかを確認するとします。チェックのためにツリーの特定のパスを選択します。ノードツリーを上にたどって、div が 2 つしかなく、ルールが適用されないことを確認する必要があるかもしれません。その場合は、ツリー内の他のパスを試す必要があります。

  3. ルールの適用には、ルールの階層を定義する非常に複雑なカスケード ルールが関係します。

ブラウザがこれらの問題にどのように対処しているかを見てみましょう。

スタイルデータの共有

WebKit ノードはスタイル オブジェクト(RenderStyle)を参照します。これらのオブジェクトは、特定の条件下でノード間で共有できます。ノードが兄弟またはいとこであり、次のいずれかに該当する。

  1. 要素は同じマウス状態である必要があります(たとえば、一方が :hover 状態でもう一方がそうでない状態は不可)。
  2. どちらの要素にも ID を指定しないでください。
  3. タグ名が一致している
  4. クラス属性が一致している
  5. マッピングされた属性のセットは同一である必要があります
  6. リンクの状態が一致している必要があります
  7. フォーカス状態は一致している必要があります
  8. どちらの要素も属性セレクタの影響を受けないようにする必要があります。影響を受けるとは、セレクタ内の任意の位置で属性セレクタを使用するセレクタの一致があることを意味します。
  9. 要素にインライン スタイル属性がない
  10. 兄弟セレクタは一切使用しないでください。WebCore は、兄弟セレクタが検出されるとグローバル スイッチをスローし、兄弟セレクタが存在する場合はドキュメント全体のスタイル共有を無効にします。これには、+ セレクタや、:first-child、:last-child などのセレクタが含まれます。

Firefox のルールツリー

Firefox には、スタイル計算を容易にするために、ルールツリーとスタイル コンテキスト ツリーという 2 つの追加ツリーがあります。 WebKit にもスタイル オブジェクトがありますが、スタイル コンテキスト ツリーのようなツリーには保存されず、DOM ノードのみが関連するスタイルを指します。

Firefox スタイルのコンテキスト ツリー。
図 14: Firefox のスタイル コンテキスト ツリー。

スタイル コンテキストには終了値が含まれます。値は、一致するすべてのルールを正しい順序で適用し、論理値から具体的な値に変換する操作を実行することで計算されます。たとえば、論理値が画面の割合である場合、計算されて絶対単位に変換されます。ルールツリーのアイデアは非常に優れています。これにより、これらの値をノード間で共有して、再計算を回避できます。これにより、スペースも節約できます。

一致したルールはすべてツリーに保存されます。パスの下位ノードの優先度が高くなります。ツリーには、見つかったルールの一致のすべてのパスが含まれます。ルールの保存は遅延して行われます。ツリーは、最初にすべてのノードに対して計算されるのではなく、ノードのスタイルを計算する必要があるときに、計算されたパスがツリーに追加されます。

この考え方は、ツリーパスを語彙の単語と見なすことです。たとえば、次のルールツリーがすでに計算されているとします。

計算されたルールツリー
図 15: 計算されたルールツリー。

コンテンツ ツリー内の別の要素のルールを照合する必要があり、一致したルールが(正しい順序で)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 構造体には、color という 1 つのメンバーのみが含まれます。 margin 構造体には、4 つの辺が含まれます。

結果のルールツリーは次のようになります(ノードにはノード名、つまりノードが指すルールの番号が付けられています)。

ルールツリー
図 16: ルールツリー

コンテキスト ツリーは次のようになります(ノード名: ルールノードが指すノード)。

コンテキスト ツリー。
図 17: コンテキスト ツリー

HTML を解析して 2 番目の <div> タグに到達したとします。このノードのスタイル コンテキストを作成し、スタイル構造体を埋める必要があります。

ルールを照合すると、<div> の一致するルールは 1、2、6 であることがわかります。つまり、要素が使用できる既存のパスがツリーにすでに存在し、ルール 6(ルールツリーのノード F)のために別のノードを追加するだけで済みます。

スタイル コンテキストを作成し、コンテキスト ツリーに配置します。新しいスタイル コンテキストは、ルールツリーのノード F を参照します。

次に、スタイル構造体を埋める必要があります。まず、マージン構造体を埋めます。最後のルールノード(F)はマージン構造体に追加されないため、以前のノード挿入で計算されたキャッシュに保存された構造体が見つかるまでツリーを上に移動して、それを使用できます。マージンルールを指定した最上位のノードであるノード B で見つかります。

color 構造体の定義があるため、キャッシュに保存された構造体は使用できません。color には 1 つの属性があるため、他の属性を入力するためにツリーを上に移動する必要はありません。終了値を計算し(文字列を RGB に変換するなど)、計算された構造体をこのノードにキャッシュします。

2 つ目の <span> 要素の作業はさらに簡単です。ルールを照合すると、前のスパンと同様にルール G を指しているという結論に達します。同じノードを指す兄弟があるため、スタイル コンテキスト全体を共有し、前のスパンのコンテキストを指すだけで済みます。

親から継承されたルールを含む構造体の場合、キャッシュはコンテキスト ツリーで行われます(color プロパティは実際には継承されますが、Firefox はそれをリセットとして扱い、ルールツリーにキャッシュします)。

たとえば、段落内のフォントにルールを追加した場合、

p {font-family: Verdana; font size: 10px; font-weight: bold}

次に、コンテキスト ツリー内の div の子である段落要素は、親と同じフォント構造体を共有できます。これは、段落にフォントルールが指定されていない場合です。

ルールツリーのない WebKit では、一致した宣言が 4 回走査されます。まず、重要でない優先度の高いプロパティが適用されます(他のプロパティが依存しているため、最初に適用する必要があるプロパティ(display など))。次に、優先度の高い重要なルール、優先度が通常の重要でないルール、優先度が通常の重要なルールが適用されます。つまり、複数回表示されるプロパティは、正しいカスケード順序に従って解決されます。最後のものが優先されます。

まとめると、スタイル オブジェクト(全体または内部の構造体の一部)を共有することで、問題 1 と 3 が解決されます。Firefox のルールツリーは、プロパティを正しい順序で適用する際にも役立ちます。

ルールを操作して簡単に一致させる

スタイルルールにはいくつかのソースがあります。

  1. 外部スタイルシートまたはスタイル要素の CSS ルール。 css p {color: blue}
  2. html <p style="color: blue" /> などのインライン スタイル属性
  3. HTML の視覚属性(関連するスタイルルールにマッピングされる) html <p bgcolor="blue" /> 最後の 2 つは、スタイル属性を所有しており、HTML 属性を要素をキーとしてマッピングできるため、要素に簡単に一致させることができます。

問題 #2 で前述したように、CSS ルールのマッチングは複雑になることがあります。この問題を解決するために、ルールを操作してアクセスを容易にします。

スタイルシートを解析した後、セレクタに応じて、ルールが複数のハッシュマップのいずれかに追加されます。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 属性ですが、より一般的)に対応するプロパティがあります。一致したルールのいずれでもプロパティが定義されていない場合、一部のプロパティは親要素のスタイル オブジェクトから継承されます。他のプロパティにはデフォルト値があります。

問題は、複数の定義がある場合に発生します。この問題を解決するために、カスケード順序が導入されました。

スタイル プロパティの宣言は、複数のスタイルシートに記述されることも、1 つのスタイルシート内に複数回記述されることもあります。つまり、ルールを適用する順序が非常に重要です。これは「カスケード」順序と呼ばれます。 CSS2 仕様によると、カスケード順序は次のとおりです(低い順)。

  1. ブラウザの宣言
  2. ユーザーの通常の宣言
  3. 通常の宣言を作成する
  4. 重要な宣言を作成する
  5. ユーザーの重要な宣言

ブラウザの宣言は最も重要度が低く、ユーザーは宣言が重要としてマークされている場合にのみ作成者をオーバーライドします。 同じ順序の宣言は、限定性、指定された順序の順に並べ替えられます。HTML の視覚属性は、対応する CSS 宣言に変換されます。これらは優先度の低い作成者ルールとして扱われます。

特異性

セレクタの詳細度は、CSS2 仕様で次のように定義されています。

  1. 宣言がセレクタを含むルールではなく「style」属性からのものである場合は 1、それ以外の場合は 0(= a)
  2. セレクタ内の ID 属性の数をカウントする(= b)
  3. セレクタ内の他の属性と疑似クラスの数をカウントする(= c)
  4. セレクタ内の要素名と疑似要素の数をカウントします(= 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 は、すべての最上位スタイルシート(@import を含む)が読み込まれたかどうかを示すフラグを使用します。アタッチ時にスタイルが完全に読み込まれていない場合は、プレースホルダが使用され、ドキュメントにマークが付けられます。スタイルシートが読み込まれると、プレースホルダは再計算されます。

レイアウト

レンダラが作成されてツリーに追加された時点では、位置とサイズは設定されていません。これらの値を計算することを、レイアウトまたはリフローと呼びます。

HTML はフローベースのレイアウト モデルを使用します。つまり、ほとんどの場合、1 回のパスでジオメトリを計算できます。通常、「フロー内」で後にある要素は、「フロー内」で前にある要素のジオメトリに影響しないため、レイアウトはドキュメント全体で左から右、上から下へと進みます。例外もあります。たとえば、HTML テーブルでは複数のパスが必要になる場合があります。

座標系はルートフレームを基準としています。上部と左側の座標が使用されます。

レイアウトは再帰的なプロセスです。これは、HTML ドキュメントの <html> 要素に対応するルート レンダラから始まります。レイアウトは、フレーム階層の一部またはすべてを再帰的に処理し、必要な各レンダラの幾何学的情報を計算します。

ルート レンダラの配置は 0,0 で、そのサイズはビューポート(ブラウザ ウィンドウの表示部分)です。

すべてのレンダラには「レイアウト」または「リフロー」メソッドがあり、各レンダラはレイアウトが必要な子のレイアウト メソッドを呼び出します。

ダーティビット システム

小さな変更のたびに完全なレイアウトを行わないように、ブラウザは「ダーティビット」システムを使用します。変更または追加されたレンダラは、自身とその子を「ダーティ」(レイアウトが必要)としてマークします。

フラグには「ダーティ」と「子がダーティ」の 2 つがあります。後者は、レンダラ自体は問題ないものの、レイアウトが必要な子が少なくとも 1 つあることを意味します。

グローバル レイアウトと増分レイアウト

レイアウトは、レンダリング ツリー全体でトリガーできます。これは「グローバル」レイアウトです。これは、次のことが原因で発生する可能性があります。

  1. フォントサイズの変更など、すべてのレンダラに影響するグローバル スタイルの変更。
  2. 画面のサイズ変更の結果

レイアウトは増分的に行うことができます。ダーティ レンダラのみがレイアウトされます(これにより、追加のレイアウトが必要になる損傷が発生する可能性があります)。

増分レイアウトは、レンダラがダーティな場合に(非同期で)トリガーされます。たとえば、ネットワークから追加のコンテンツが取得され、DOM ツリーに追加された後に、新しいレンダラがレンダリング ツリーに追加される場合などです。

増分レイアウト。
図 18: 増分レイアウト - ダーティなレンダラとその子のみがレイアウトされる

非同期レイアウトと同期レイアウト

増分布置は非同期で行われます。Firefox は、増分レイアウトの「リフロー コマンド」をキューに入れ、スケジューラがこれらのコマンドのバッチ実行をトリガーします。WebKit には、インクリメンタル レイアウトを実行するタイマーもあります。ツリーが走査され、「ダーティ」なレンダラがレイアウトされます。

「offsetHeight」などのスタイル情報をリクエストするスクリプトは、増分レイアウトを同期的にトリガーできます。

グローバル レイアウトは通常、同期的にトリガーされます。

スクロール位置の変更など、一部の属性が変更されたために、最初のレイアウト後にコールバックとしてレイアウトがトリガーされることがあります。

最適化

レイアウトが「サイズ変更」またはレンダラの(サイズではなく)位置の変更によってトリガーされると、レンダラのサイズはキャッシュから取得され、再計算されません。

場合によっては、サブツリーのみが変更され、レイアウトがルートから開始されないことがあります。これは、変更がローカルで、周囲に影響しない場合に発生します。たとえば、テキスト フィールドに挿入されたテキストなどです(そうでない場合、キーストロークごとにルートからレイアウトがトリガーされます)。

レイアウト プロセス

通常、レイアウトは次のパターンになります。

  1. 親レンダラが独自の幅を決定します。
  2. 保護者がお子様と一緒に以下を行います。
    1. 子レンダラを配置します(x と y を設定します)。
    2. 必要に応じて(ダーティな場合、グローバル レイアウトの場合、またはその他の理由で)子レイアウトを呼び出し、子の高さを計算します。
  3. 親は、子の累積の高さと、マージンとパディングの高さを使用して、自身の高さを設定します。これは、親レンダラの親によって使用されます。
  4. ダーティビットを false に設定します。

Firefox は、レイアウト(「リフロー」と呼ばれる)のパラメータとして「状態」オブジェクト(nsHTMLReflowState)を使用します。状態には、親の幅などが含まれます。

Firefox のレイアウトの出力は「metrics」オブジェクト(nsHTMLReflowMetrics)です。レンダラで計算された高さが含まれます。

幅の計算

レンダラの幅は、コンテナ ブロックの幅、レンダラのスタイル「width」プロパティ、マージン、枠線を使用して計算されます。

たとえば、次の div の幅は、

<div style="width: 30%"/>

WebKit では次のように計算されます(class RenderBox method calcWidth)。

  • コンテナの幅は、コンテナの availableWidth と 0 の最大値です。この場合の availableWidth は contentWidth で、次のように計算されます。
clientWidth() - paddingLeft() - paddingRight()

clientWidth と clientHeight は、枠線とスクロールバーを除くオブジェクトの内部を表します。

  • 要素の幅は「width」スタイル属性です。コンテナの幅の割合を計算して、絶対値として計算されます。

  • 水平方向の枠線とパディングが追加されました。

ここまでは「優先幅」の計算でした。これで、最小幅と最大幅が計算されます。

優先幅が最大幅より大きい場合は、最大幅が使用されます。最小幅(分割できない最小単位)より小さい場合は、最小幅が使用されます。

値は、レイアウトが必要な場合に備えてキャッシュに保存されますが、幅は変更されません。

改行

レイアウトの途中でレンダラが改行が必要であると判断すると、レンダラは停止し、改行が必要であることをレイアウトの親に伝播します。親は追加のレンダラを作成し、それらに対してレイアウトを呼び出します。

絵画

ペイント ステージでは、レンダリング ツリーが走査され、レンダラの「paint()」メソッドが呼び出されて、コンテンツが画面に表示されます。 ペイントでは、UI インフラストラクチャ コンポーネントが使用されます。

グローバルと増分

レイアウトと同様に、ペイントもグローバル(ツリー全体がペイントされる)または増分にすることができます。インクリメンタル ペインティングでは、一部のレンダラがツリー全体に影響しない方法で変更されます。変更されたレンダラは、画面上の長方形を無効にします。これにより、OS はこれを「ダーティ リージョン」と見なし、「ペイント」イベントを生成します。OS はこれを巧みに行い、複数のリージョンを 1 つにまとめます。Chrome では、レンダラがメインプロセスとは異なるプロセスにあるため、より複雑になります。Chrome は OS の動作をある程度シミュレートします。 プレゼンテーションはこれらのイベントをリッスンし、メッセージをレンダリング ルートに委任します。関連するレンダラに到達するまでツリーが走査されます。それ自体(通常はその子も)を再描画します。

描画順序

CSS2 では、描画プロセスの順序が定義されています。 これは、実際にはスタッキング コンテキストで要素が積み重ねられる順序です。スタックは背面から前面に描画されるため、この順序は描画に影響します。ブロック レンダラの重ね順は次のとおりです。

  1. 背景色
  2. 背景画像
  3. border
  4. 子供
  5. アウトライン

Firefox の表示リスト

Firefox は、レンダリング ツリーを走査して、描画された長方形の表示リストを作成します。これには、長方形に関連するレンダラが、正しい描画順序(レンダラの背景、枠線など)で含まれています。

これにより、再描画のためにツリーを走査する必要があるのは 1 回だけで、複数回(すべての背景、すべての画像、すべての枠線など)走査する必要はありません。

Firefox は、他の不透明な要素の下に完全に隠れる要素など、非表示になる要素を追加しないことで、プロセスを最適化します。

WebKit の長方形ストレージ

再描画する前に、WebKit は古い長方形をビットマップとして保存します。次に、新しい長方形と古い長方形の差分のみをペイントします。

動的な変更

ブラウザは、変更に応じて可能な限り最小限のアクションを実行しようとします。そのため、要素の色を変更すると、その要素のみが再描画されます。要素の位置を変更すると、その要素、子要素、場合によっては兄弟要素のレイアウトと再描画が発生します。 DOM ノードを追加すると、ノードのレイアウトと再描画が発生します。「html」要素のフォントサイズを大きくするなどの大きな変更を行うと、キャッシュが無効になり、ツリー全体の再レイアウトと再描画が発生します。

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

レンダリング エンジンはシングル スレッドです。ネットワーク オペレーションを除くほぼすべてが、単一のスレッドで行われます。Firefox と Safari では、これはブラウザのメインスレッドです。Chrome では、タブプロセスのメインスレッドです。

ネットワーク オペレーションは、複数の並列スレッドで実行できます。並列接続の数が制限されている(通常は 2 ~ 6 接続)。

イベントループ

ブラウザのメインスレッドはイベントループです。 これは、プロセスを維持する無限ループです。イベント(レイアウト イベントやペイント イベントなど)を待機して処理します。以下は、メイン イベントループの Firefox コードです。

while (!mExiting)
    NS_ProcessNextEvent(thread);

CSS2 ビジュアル モデル

キャンバス

CSS2 仕様によると、キャンバスという用語は「フォーマット構造がレンダリングされるスペース」、つまりブラウザがコンテンツを描画する場所を指します。

キャンバスは空間の各ディメンションに対して無限ですが、ブラウザはビューポートのディメンションに基づいて初期幅を選択します。

www.w3.org/TR/CSS2/zindex.html によると、キャンバスは別のキャンバスに含まれている場合は透明になり、含まれていない場合はブラウザで定義された色になります。

CSS ボックスモデル

CSS ボックスモデルは、ドキュメント ツリー内の要素に対して生成され、視覚的な書式設定モデルに従ってレイアウトされる長方形のボックスを記述します。

各ボックスには、コンテンツ領域(テキスト、画像など)と、オプションの周囲のパディング、ボーダー、マージン領域があります。

CSS2 ボックスモデル
図 19: CSS2 ボックスモデル

各ノードは、このようなボックスを 0 ~ n 個生成します。

すべての要素には、生成されるボックスのタイプを決定する「display」プロパティがあります。

例:

block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.

デフォルトはインラインですが、ブラウザのスタイルシートで他のデフォルトが設定されている場合があります。たとえば、「div」要素のデフォルトの表示はブロックです。

デフォルトのスタイルシートの例については、www.w3.org/TR/CSS2/sample.html をご覧ください。

ポジショニング スキーム

次の 3 つのスキームがあります。

  1. 標準: オブジェクトはドキュメント内の位置に従って配置されます。つまり、レンダリング ツリー内の位置は DOM ツリー内の位置と同じであり、ボックスのタイプと寸法に従ってレイアウトされます。
  2. float: オブジェクトは、まず通常のフローのようにレイアウトされ、その後、可能な限り左または右に移動されます。
  3. absolute: オブジェクトは、DOM ツリーとは異なる場所のレンダリング ツリーに配置されます。

配置スキームは、「position」プロパティと「float」属性によって設定されます。

  • static と relative は通常のフローを発生させる
  • absolute と fixed は絶対位置指定

静的位置指定では位置が定義されず、デフォルトの位置指定が使用されます。他のスキームでは、作成者が位置(上、下、左、右)を指定します。

ボックスのレイアウト方法は、次の要素によって決まります。

  • Box タイプ
  • 箱の寸法
  • ポジショニング スキーム
  • 画像サイズや画面サイズなどの外部情報

ボックスタイプ

ブロック ボックス: ブロックを形成します。ブラウザ ウィンドウに独自の長方形があります。

ブロックボックス。
図 20: ブロック ボックス

インライン ボックス: 独自のブロックはなく、コンテナ ブロック内にあります。

インライン ボックス。
図 21: インライン ボックス

ブロックは縦方向に 1 つずつフォーマットされます。インラインは横方向にフォーマットされます。

ブロックとインラインの書式設定。
図 22: ブロック形式とインライン形式

インライン ボックスは、行または「行ボックス」内に配置されます。行の高さは、最も高いボックスと同じかそれ以上になります。ボックスが「ベースライン」に揃えられている場合、つまり要素の下部が別のボックスの下部以外の位置に揃えられている場合は、行の高さが最も高いボックスよりも高くなることがあります。 コンテナの幅が十分でない場合、インラインは複数の行に配置されます。通常、段落ではこの処理が行われます。

行。
図 23: 線

ポジショニング

相対

相対位置 - 通常どおりに配置してから、必要なデルタだけ移動します。

相対位置。
図 24: 相対位置

浮動小数点

フロート ボックスは、行の左または右にシフトされます。興味深い特徴は、他のボックスがその周りを流れることです。HTML:

<p>
  <img style="float: right" src="images/image.gif" width="100" height="100">
  Lorem ipsum dolor sit amet, consectetuer...
</p>

次のようになります。

浮動小数。
図 25: フロート

絶対値と固定値

レイアウトは、通常のフローに関係なく正確に定義されます。この要素は通常のフローには関与しません。 寸法はコンテナを基準にしています。fixed の場合、コンテナはビューポートです。

固定位置。
図 26: 固定位置指定

階層化された表現

これは、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>

結果は次のようになります。

固定位置。
図 27: 固定位置

マークアップでは赤の div が緑の div の前にあり、通常のフローでは先に描画されますが、z-index プロパティが高いため、ルートボックスが保持するスタックではより前面に配置されます。

リソース

  1. ブラウザのアーキテクチャ

    1. Grosskurth, Alan. ウェブブラウザのリファレンス アーキテクチャ(pdf)
    2. Gupta, Vineet. ブラウザの仕組み - パート 1 - アーキテクチャ
  2. 解析

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (aka the "Dragon book"), Addison-Wesley, 1986
    2. Rick Jelliffe。The Bold and the Beautiful: two new drafts for HTML 5.
  3. Firefox

    1. L.David Baron、Faster HTML and CSS: Layout Engine Internals for ウェブ デベロッパー。
    2. L.David Baron、Faster HTML and CSS: Layout Engine Internals for ウェブ デベロッパー(Google テックトーク動画)
    3. L.David Baron、Mozilla のレイアウト エンジン
    4. L.David Baron、Mozilla Style System Documentation
    5. Chris Waterson、Notes on HTML Reflow
    6. Chris Waterson、Gecko Overview
    7. Alexander Larsson、HTML HTTP リクエストのライフサイクル
  4. Webkit

    1. David Hyatt、Implementing CSS(part 1)
    2. David Hyatt、An Overview of WebCore
    3. David Hyatt、WebCore Rendering
    4. David Hyatt、The FOUC Problem
  5. W3C 仕様

    1. HTML 4.01 仕様
    2. W3C HTML5 仕様
    3. カスケーディング スタイル シート Level 2 Revision 1(CSS 2.1)仕様
  6. ブラウザのビルド手順

    1. Firefox。https://developer.mozilla.org/Build_Documentation
    2. WebKit。http://webkit.org/building/build.html

翻訳

このページは日本語に 2 回翻訳されています。

外部でホストされている韓国語とトルコ語の翻訳を表示できます。

みなさんもおつかれさまでした。