ブラウザの仕組み

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

序文

WebKit と Gecko の内部オペレーションに関するこの包括的な入門ガイドは、 イスラエルのデベロッパー Tali Garsiel 氏による多数の研究の結果です。1 ~ 2、3 ブラウザの内部構造に関する公開データをすべて確認し、 あまり時間を費やすことはありません。彼女は次のように書いています。

ウェブ デベロッパーとしてブラウザの操作の仕組みを学ぶ より適切な意思決定を行い、開発の背後にある正当性を理解するのに役立つ ベスト プラクティスをご覧ください。これはかなり長いドキュメントですが、Google に 時間をかけて調査を進めていきます。できてよかったね。

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

はじめに

ウェブブラウザは最も広く使用されているソフトウェアです。この入門編では 舞台裏で働きます「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(World Wide Web Consortium)組織によって管理されています。何年もの間、ブラウザは仕様の一部にのみ適合し、独自の拡張機能を開発していました。そのため、ウェブの作成者にとって重大な互換性の問題を引き起こしました。現在、ほとんどのブラウザはほぼこの仕様に準拠しています。

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

  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 などのストレージ メカニズムもサポートしています。
で確認できます。 <ph type="x-smartling-placeholder">
</ph> ブラウザのコンポーネント
図 1: ブラウザ コンポーネント

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 のチャンクで行われます。

その後のレンダリング エンジンの基本的なフローは次のとおりです。

<ph type="x-smartling-placeholder">
</ph> レンダリング エンジンの基本フロー
図 2: レンダリング エンジンの基本フロー

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

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

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

この手続きは段階的に行われます。ユーザー エクスペリエンスを向上させるため、レンダリング エンジンは可能な限り早くコンテンツを画面に表示しようとします。 すべての HTML が解析されるのを待ってから、レンダリング ツリーの作成とレイアウトを開始します。 コンテンツの一部が解析されて表示されますが、ネットワークから継続的に送信される残りのコンテンツの処理を続行します。

メインフローの例

<ph type="x-smartling-placeholder">
</ph> WebKit のメインフロー。
図 3: WebKit のメインフロー
<ph type="x-smartling-placeholder">
</ph> Mozilla の Gecko レンダリング エンジンのメインフロー。
図 4: Mozilla の Gecko レンダリング エンジンのメインフロー

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

Gecko では、視覚的に書式設定された要素のツリーを「フレームツリー」と呼んでいます。各要素がフレームです。 WebKit では「レンダリング ツリー」という用語が使用されている「レンダリング オブジェクト」で構成されています。 WebKit では「レイアウト」という用語が使用されているGecko では「リフロー」と呼んでいます。 「添付ファイル」は、DOM ノードと視覚的な情報を結び付けてレンダリング ツリーを作成することを指す、WebKit の用語です。 セマンティックではない小さな違いは、Gecko では HTML と DOM ツリーの間にレイヤが追加されていることです。これは「コンテンツ シンク」と呼ばれ、DOM 要素を作成するためのファクトリです フローの各部分について説明します。

解析 - 一般

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

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

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

<ph type="x-smartling-placeholder">
</ph> 数式のツリーノード。
図 5: 数式のツリーノード

文法

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

パーサーとレクサーの組み合わせ

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

語彙分析は、入力をトークンに分割するプロセスです。 トークンは言語語彙、つまり有効な構成要素のコレクションです。人間の言語では、その言語の辞書に出てくるすべての単語で構成されます。

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

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

レクサーは、空白や改行などの不要な文字を削除する方法を認識しています。

<ph type="x-smartling-placeholder">
</ph> ソース ドキュメントから解析ツリーへ
図 6: ソース ドキュメントから解析ツリーへ

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

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

翻訳

多くの場合、解析ツリーは最終積ではありません。解析は翻訳によく使用されます。つまり、入力ドキュメントを別の形式に変換します。その一例はコンパイルです。ソースコードをマシンコードにコンパイルするコンパイラは、まずソースコードを解析して解析ツリーを作成し、ツリーをマシンコード ドキュメントに変換します。

<ph type="x-smartling-placeholder">
</ph> コンパイル フロー
図 7: コンパイル フロー

解析の例

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

構文:

  1. 言語の構文構成要素は、式、用語、演算です。
  2. 使用する言語には任意の数の式を含めることができます。
  3. 式は「term」として定義されるその後に「Operation」がその後に別の項が続きます
  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 で完全に表現できる文法です。 正式な定義については、 文脈自由文法に関するウィキペディアの記事

パーサーの種類

パーサーには、トップダウン パーサーとボトムアップ パーサーの 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 の文法は文脈フリーではないため、従来のパーサーでは 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 要素のインターフェースです。

ツリーのルートは「ドキュメント」です。渡されます。

DOM はマークアップとほぼ 1 対 1 の関係にあります。 例:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

このマークアップは次の DOM ツリーに変換されます。

<ph type="x-smartling-placeholder">
</ph> サンプル マークアップの 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 インターフェースの 1 つを実装する要素で構成されていることを意味します。ブラウザが実際に使用する実装には、ブラウザによって内部で使用される他の属性があります。

解析アルゴリズム

前のセクションで説明したように、通常のトップダウンまたはボトムアップのパーサーを使用して HTML を解析することはできません。

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

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

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

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

トークン化は語彙分析であり、入力を解析してトークンにします。 HTML トークンには、開始タグ、終了タグ、属性名、属性値があります。

トークナイザはトークンを認識してツリー コンストラクタに渡し、その次の文字を消費して次のトークンを認識します。この文字が入力が終了するまで続きます。

<ph type="x-smartling-placeholder">
</ph> HTML 解析フロー(HTML5 仕様から取得)
図 9: HTML 解析フロー(HTML5 仕様より引用)

トークン化アルゴリズム

アルゴリズムの出力は HTML トークンです。 このアルゴリズムはステートマシンとして表現されます。 各状態は入力ストリームの 1 つ以上の文字を使用し、それらの文字に従って次の状態を更新します。 この判断は、現在のトークン化の状態とツリー構築の状態に影響されます。つまり、同じキャラクターを使用しても、現在の状態に応じて、次の状態では異なる結果が得られます。 このアルゴリズムは複雑すぎて完全に説明できません。原則を理解するのに役立つ簡単な例を見てみましょう。

基本的な例 - 次の HTML のトークン化。

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

初期状態は「データ状態」です。 文字 < が検出されると、状態は「タグ開始状態」に変更されます。 a-z 文字を使用すると「開始タグトークン」が作成され、状態が「タグ名の状態」に変わります。 > 文字が消費されるまでこの状態を維持します。各文字が新しいトークン名に追加されます。この場合、作成されるトークンは html トークンです。

> タグに達すると、現在のトークンが発行され、状態が「データ状態」に戻ります。 <body> タグも同じ手順で処理されます。 ここまでで、html タグと body タグが発行されています。これで「データ状態」に戻ります。 Hello worldH 文字を使用すると、文字トークンの作成と出力が行われます。これは </body>< に達するまで続きます。Hello world の文字ごとに文字トークンを出力します。

再び「タグ開始状態」に戻ります。 次の入力 / を使用すると、end tag token が作成され、「タグ名の状態」に移行します。再び > に達するまでこの状態を維持します。その後、新しいタグトークンが発行され、「データ状態」に戻ります。 </html> の入力は、前の場合と同様に処理されます。

<ph type="x-smartling-placeholder">
</ph> 入力例のトークン化
図 10: 入力例のトークン化

ツリー構築アルゴリズム

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

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

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

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

状態は "before head" に変更されます。「本文」トークンを受け取ります。HTMLHeadElement は、「head」はありませんが、暗黙的に作成されます。ツリーに追加されます。

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

「Hello World」の文字トークン文字列が受信されるようになりました。1 つ目は「Text」の作成と挿入ですその他の文字はそのノードに追加されます。

body end トークンを受け取ると、"after body" モードに移行します。 これで html 終了タグを受け取り、"after after body" モードに移行します。 ファイル終了トークンを受け取ると、解析が終了します。

<ph type="x-smartling-placeholder">
</ph> サンプル HTML のツリー構築。
図 11: サンプル html のツリー構築

解析完了時のアクション

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

トークン化とツリー構築のアルゴリズム全体については、HTML5 仕様をご覧ください

ブラウザエラー トレランス

「無効な構文」と表示されることがないエラーが表示されます。 ブラウザは無効なコンテンツを修正し、そのまま処理を続けます。

次の 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}*

&quot;ident&quot;識別子の略です。 "名前"「#」で参照される要素 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.errora.error はセレクタです。中かっこ内の部分には、このルールセットによって適用されるルールが含まれています。 この構造は次の定義で正式に定義されています。

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;

つまり、ルールセットは 1 つのセレクタ、またはオプションでカンマとスペースで区切られた複数のセレクタです(S は空白を表します)。 ルールセットには中かっこが含まれ、その中に 1 つの宣言のほか、必要に応じてセミコロンで区切られた複数の宣言が含まれます。 「宣言」「selector」と入力しますは、以下の BNF 定義で定義されます。

WebKit CSS パーサー

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

<ph type="x-smartling-placeholder">
</ph> CSS の解析。
図 12: CSS の解析

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

スクリプト

ウェブのモデルは同期的です。作成者は、パーサーが <script> タグに到達するとすぐにスクリプトが解析され、実行されることを期待しています。 ドキュメントの解析は、スクリプトが実行されるまで停止します。 スクリプトが外部の場合、まずリソースをネットワークから取得する必要があります。この処理も同期的に行われ、リソースが取得されるまで解析は停止します。 これは長年のモデルであり、HTML4 と HTML5 の仕様でも規定されています。 作成者は "defer" オプションを属性をスクリプトに渡す場合、ドキュメントの解析は停止せず、ドキュメントの解析後に実行されます。HTML5 には、スクリプトを非同期としてマークするオプションが追加され、スクリプトが解析されて別のスレッドで実行されるようになります。

投機的解析

WebKit と Firefox の両方で、この最適化が行われます。スクリプトの実行中、別のスレッドがドキュメントの残りの部分を解析し、ネットワークから読み込む必要のある他のリソースを見つけ出して読み込みます。このようにして、リソースを並列接続で読み込むことができ、全体的な速度が向上します。注: 投機的パーサーは、外部スクリプト、スタイルシート、画像などの外部リソースへの参照のみを解析します。DOM ツリーは変更されません。その部分はメインパーサーに委ねられます。

スタイルシート

一方、スタイルシートはモデルが異なります。 概念的には、スタイルシートによって DOM ツリーは変更されないため、スタイルシートの解析を待ってドキュメントの解析を停止する理由はないようです。ただし、ドキュメント解析の段階でスタイル情報を要求するスクリプトには問題があります。 スタイルの読み込みと解析がまだ完了していない場合、スクリプトは誤った回答を返し、多くの問題を引き起こします。 特殊なケースのように思えますが、ごく一般的なケースです。 Firefox では、読み込みと解析が完了していないスタイルシートがある場合、すべてのスクリプトがブロックされます。 WebKit は、アンロードされたスタイルシートの影響を受ける可能性のある特定のスタイル プロパティにアクセスしようとした場合にのみ、スクリプトをブロックします。

レンダリング ツリーの構築

DOM ツリーの作成中に、ブラウザは別のツリー(レンダリング ツリー)を作成します。 このツリーには、視覚要素が表示される順番に並んでいます。 ドキュメントを視覚的に表したものです。 このツリーの目的は、コンテンツを正しい順序で描画できるようにすることです。

Firefox では、レンダリング ツリー内の要素を「frames」と呼びます。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 属性の値。スタイルの計算のセクションをご覧ください。 次の 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」に割り当てられた要素です。は表示されません(公開設定が「非表示」の要素はツリーに表示されます)。

複数のビジュアル オブジェクトに対応する DOM 要素があります。これらの要素は通常、単一の長方形では説明できない複雑な構造を持つ要素です。たとえば、「select」コマンドを要素には 3 つのレンダラがあります。1 つは表示領域用、もう 1 つはプルダウン リスト ボックス用、もう 1 つはボタン用です。 また、幅が 1 行には不十分であるためにテキストが複数行に分かれている場合も、新しい行が追加のレンダラとして追加されます。

複数のレンダラのもう一つの例として、破損した HTML があります。 CSS 仕様では、インライン要素にはブロック要素のみ、またはインライン要素のみを含める必要があります。 混合コンテンツの場合は、インライン要素をラップするために匿名のブロック レンダラが作成されます。

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

<ph type="x-smartling-placeholder">
</ph> レンダリング ツリーとそれに対応する DOM ツリー。
図 13: レンダリング ツリーと対応する DOM ツリー「ビューポート」最初の包含ブロックですWebKit では「RenderView」オブジェクト

ツリーを構築するフロー

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

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

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

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

スタイルの計算

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

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

スタイルシートのベースは、ブラウザのデフォルトのスタイルシート、ページ作成者が提供したスタイルシート、およびユーザー スタイルシートです。これらはブラウザのユーザーが提供するスタイルシートです(ブラウザで任意のスタイルを定義できます)。たとえば Firefox では、「Firefox のプロファイル」セクションにスタイルシートを配置して、フォルダです。

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

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

    例 - この複合セレクタの場合:

    div div div div{
    ...
    }
    

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

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

各ブラウザでこれらの問題がどのように発生するかを見てみましょう。

スタイルデータの共有

WebKit のノードはスタイル オブジェクト(RenderStyle)を参照します。 状況によっては、これらのオブジェクトはノードで共有されます。 ノードが兄弟またはいと日で、次の場合:

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

Firefox のルールツリー

Firefox には、ルール ツリーとスタイル コンテキスト ツリーという 2 つのツリーが追加されており、スタイルを簡単に計算できます。 WebKit にもスタイル オブジェクトがありますが、スタイル コンテキスト ツリーのようなツリーには保存されず、関連するスタイルをポイントするのは DOM ノードだけです。

<ph type="x-smartling-placeholder">
</ph> Firefox のスタイル コンテキスト ツリー。
図 14: Firefox のスタイル コンテキスト ツリー

スタイル コンテキストには終了値が含まれます。すべての照合ルールを正しい順序で適用し、論理値から具体的な値に変換する操作によって値が計算されます。たとえば、論理値が画面に対するパーセンテージの場合は、論理値が計算され、絶対単位に変換されます。 ルールツリーはとても巧妙です。これらの値をノード間で共有できるため、再度計算する必要がなくなります。スペースの節約にもなります。

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

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

<ph type="x-smartling-placeholder">
</ph> 計算されたルールツリー
図 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 つの辺があります。

作成されるルールツリーは次のようになります(ノードにはノード名(参照しているルールの数)が表示されます)。

<ph type="x-smartling-placeholder">
</ph> ルールツリー
図 16: ルールツリー

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

<ph type="x-smartling-placeholder">
</ph> コンテキスト ツリー。
図 17: コンテキスト ツリー

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 のルールツリーを使用すると、プロパティを正しい順序で適用することもできます。

一致しやすくするためのルールの操作

スタイル規則には次のような情報源があります。

  1. 外部スタイルシートまたはスタイル要素内の CSS ルール。 css p {color: blue}
  2. インライン スタイル属性(例: html <p style="color: blue" />
  3. HTML ビジュアル属性(関連するスタイルルールにマッピングされている) html <p bgcolor="blue" /> 最後の 2 つは、彼がスタイル属性を所有しており、この要素をキーとして使用して HTML 属性をマッピングできるため、要素と簡単に照合できます。

先ほどの問題 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 属性)に対応するプロパティがあります。 一致したルールのいずれにもプロパティが定義されていない場合、一部のプロパティは親要素スタイル オブジェクトに継承されます。他のプロパティにはデフォルト値があります。

問題は複数の定義が存在するときに始まります。つまり、問題を解決するためのカスケード順序が現れます。

スタイル プロパティの宣言は、複数のスタイルシートで指定することも、1 つのスタイルシート内で複数回指定することもできます。 つまり、ルールを適用する順序が非常に重要です。これは「カスケード」と呼ばれるできます。 CSS2 仕様では、カスケードの順序は以下のようになっています(低い順)。

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

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

特異性

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

  1. その宣言が「スタイル」であれば 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 で、サイズはビューポート(ブラウザ ウィンドウの表示部分)です。

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

ダーティビット システム

わずかな変更のたびに完全なレイアウトが行われないようにするため、ブラウザは「ダーティビット」を使用します。ありません 変更または追加されたレンダラは、自身とその子に「ダーティ」(レイアウトが必要)のマークを付けます。

「dirty」と「children are dirty」の 2 つのフラグがあります。つまり、レンダラ自体は問題ありませんが、レイアウトを必要とする子が少なくとも 1 つあります。

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

レイアウトはレンダリング ツリー全体でトリガーできます。これは「グローバル」です。できます。 考えられる原因は次のとおりです。

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

インクリメンタル レイアウトが可能で、ダーティなレンダラのみがレイアウトされます(これにより、ダメージが発生する可能性があり、追加のレイアウトが必要になる可能性があります)。

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

<ph type="x-smartling-placeholder">
</ph> 増分レイアウト。
図 18: 増分レイアウト - ダーティなレンダラとその子のみが配置されている

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

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

"offsetHeight" などのスタイル情報を要求するスクリプト増分レイアウトを同期的にトリガーできます

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

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

最適化

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

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

レイアウト プロセス

通常、レイアウトのパターンは次のとおりです。

  1. 親レンダラが自身の幅を決定します。
  2. 親が子を管理しており、 <ph type="x-smartling-placeholder">
      </ph>
    1. 子レンダラを配置します(x と y を設定します)。
    2. 子レイアウトを必要に応じて呼び出します(ダーティである、グローバル レイアウト内にある、その他なんらかの理由で子の高さを計算するなど)。
  3. 親は、子の累積の高さ、マージンの高さ、パディングを使用して、独自の高さを設定します。これは親レンダラの親で使用されます。
  4. ダーティビットを false に設定します。

Firefox では「状態」フィールドがオブジェクト(nsHTMLReflowState)をレイアウト(「reflow」と呼びます)のパラメータとして渡します。特に、状態には親の幅が含まれます。

Firefox レイアウトの出力は「指標」(nsHTMLReflowMetrics)。これには、計算されたレンダラの高さが含まれます。

幅の計算

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

たとえば、次の div の幅などです。

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

WebKit によって次のように計算されます(RenderBox クラスの calcWidth クラス)。

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

clientWidth と clientHeight はオブジェクトの内部を表す 枠線とスクロールバーは除外されます

  • 要素の幅はスタイル属性を指定します。 コンテナの幅に占める割合を計算し、絶対値として計算されます。

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

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

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

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

改行

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

絵画

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

グローバルとインクリメンタル

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

描画順序

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

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

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 ボックスモデル

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

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

<ph type="x-smartling-placeholder">
</ph> 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. フロート: オブジェクトは通常の流れに沿って配置され、次にできるだけ左または右に移動します。
  3. 絶対的: オブジェクトはレンダー ツリーの DOM ツリーとは異なる場所に配置される

配置方法は「position」とプロパティと「float」属性です。

  • 通常のフローの原因となる
  • 「絶対原因」と「固定原因」の「絶対位置」です

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

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

  • ボックスの種類
  • 箱の寸法
  • 配置スキーム
  • 画像サイズや画面サイズなどの外部情報

ボックスの種類

ブロック ボックス: 1 つのブロックを形成し、ブラウザ ウィンドウ内に独自の長方形を表示します。

<ph type="x-smartling-placeholder">
</ph> ブロック ボックス。
図 20: ブロック ボックス

インライン ボックス: 独自のブロックはありませんが、包含ブロックの内部にあります。

<ph type="x-smartling-placeholder">
</ph> インライン ボックス。
図 21: インライン ボックス

ブロックは垂直方向に次々とフォーマットされます。 インラインは水平方向に書式設定されます。

<ph type="x-smartling-placeholder">
</ph> ブロックとインライン形式。
図 22: ブロックとインラインの書式設定

インライン ボックスは、行(ラインボックス)の中に配置します。 線の高さは、最も背の高い箱と同じくらいであるが、箱が「ベースライン」に並んでいる場合は、もっと高くすることもできる- 要素の下部が、下部以外の別のボックスの点に揃えられます。 コンテナの幅が足りない場合は、インラインが数行に配置されます。 これは通常、段落で起こります。

<ph type="x-smartling-placeholder">
</ph> 行。
図 23: 線

位置付け

相対

相対的なポジショニング - 通常と同じように配置し、必要な差分だけ移動します。

<ph type="x-smartling-placeholder">
</ph> 相対的なポジショニング。
図 24: 相対的なポジショニング

浮動小数点

フロート ボックスは、行の左または右にシフトします。興味深い点は、その周りに他のボックスが流れている点です。 HTML:

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

次のようになります。

<ph type="x-smartling-placeholder">
</ph> 浮動小数。
図 25: 浮動小数点数

絶対的、固定的

レイアウトは、通常のフローとまったく関係なく定義されます。要素は通常のフローに関与しません。 寸法はコンテナを基準とします。 固定では、コンテナはビューポートです。

<ph type="x-smartling-placeholder">
</ph> 配置は固定されています。
図 26: 固定配置

階層表示

CSS の Z-Index プロパティで指定されます。 これは、ボックスの 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>

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

<ph type="x-smartling-placeholder">
</ph> 配置は固定されています。
図 27: 固定配置

マークアップでは赤の div が緑の div よりも前に配置されており、通常のフローでも描画されているはずですが、Z-Index プロパティが高いため、ルート ボックスで保持されるスタック内では前方に配置されています。

リソース

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

    1. グロースカート、アラン。A Reference Architecture for Web Browsers(PDF)
    2. Gupta、Vineet。ブラウザの仕組み - パート 1 - アーキテクチャ
  2. 解析

    1. Aho、Sethi、Ullman、コンパイラ: 原則、テクニック、ツール(別名「ドラゴンブック」)、Addison-Wesley、1986 年
    2. リック ゼリフ。The Bold and the Beautiful: HTML 5 の 2 つの新しいドラフト。
  3. Firefox

    1. L.David Baron、Faster HTML and CSS: Layout Engine Internals for Web Developers
    2. L.David Baron、Faster HTML and CSS: Layout Engine Internals for Web Developers(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 の概要
    7. Alexander Larsson、Thelife of an HTML HTTP request
  4. Webkit

    1. David Hyatt、Implement CSS(part 1)
    2. David Hyatt、WebCore の概要
    3. David Hyatt、WebCore レンダリング
    4. David Hyatt、The FOUC Problem
  5. W3C 仕様

    1. HTML 4.01 仕様
    2. W3C HTML5 仕様
    3. Cascading Style Sheets Level 2 Revision 1(CSS 2.1) Specification
  6. ブラウザのビルド手順

    1. Firefox。https://developer.mozilla.org/Build_Documentation
    2. WebKithttp://webkit.org/building/build.html
で確認できます。

翻訳

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

Google Cloud の外部でホストされている翻訳を 韓国語トルコ語

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