【実務・中級編】 DOMツリー構築アルゴリズム – Webブラウザの仕組み実践ガイド

こんにちは。フロントエンドの現場で日々、パフォーマンスチューニングや不可解なレンダリングバグと格闘している君なら、「DOM(Document Object Model)がどうやって作られているか」を一度は意識したことがあるはずだ。

「HTMLを書いて、ブラウザがそれを読めば勝手にツリーができるんでしょ?」
――まあ、大正解だ。だが、シニアの視点から言わせてもらうと、この「勝手にやってくれる裏側の仕組み」を理解しているか否かで、書くHTMLの質、そして何よりWebアプリのパフォーマンスが劇的に変わる。

今回は、ネットワークの向こうから流れてきたバイト列が、あの厄介で愛おしい「DOMツリー」に化けるまでの泥臭い裏側と、実務で絶対に知っておくべきエラーハンドリングの仕様について、徹底的に紐解いていこう。

—

1. バイト列からDOMへ:レンダリングエンジンの裏側の舞台裏

ブラウザ(ChromiumならBlink、WebKitなど)がHTMLファイルを読み込むとき、実は一気にツリーを作っているわけじゃない。いくつかの残酷なまでのステップを踏んでいる。

1. バイト列のデコード: ネットワークから受信した生データ(`010101…`)を、指定された文字コード(UTF-8など)で文字列に変換する。
2. トークン化 (Tokenization): 文字列をW3Cの仕様(HTML5 Spec)に基づき、「開始タグ」「終了タグ」「属性」「テキスト」といった最小単位のトークンに切り分けていく。
3. ツリー構築 (Tree Construction): トークンを元にオブジェクト(Node)を生成し、親子関係(DOM)を組み立てていく。

ここで重要なのが、「HTMLのパースとDOMツリーの構築は、ストリーミング(インクリメンタル)で行われる」という点だ。
ブラウザは、HTMLファイルがすべてダウンロードされるのを待ったりしない。ネットワークからチャンク(断片)が届くたびに、絶えずトークン化し、DOMツリーを構築し、画面を描画しようとする。この「途中経過」を無理やりJavaScriptから触らせるのが、世も末なインラインスクリプトの悪夢の始まりなわけだが……それはまた別の機会にしよう。

トークン化の狂気:状態機械(State Machine)

HTMLのパース仕様書を読んだことがあるかい? あれは地獄のような「状態機械(Finite State Machine)」の塊だ。
例えば、パーサが `<` を見つけると「タグオープン状態」になり、そこからアルファベットが来ると「タグ名状態」に遷移する。もし途中で `` が来るまでひたすら文字を飲み込み続ける。

人間が書く適当なHTMLを、ブラウザが何食わぬ顔で解釈できるのは、この厳密な状態機械のおかげなのだ。

—

2. 驚異の「エラー耐性」:不正なHTMLはどう処理されるか?

実務でフロントエンドをやっていると、CMSが出力したクソみたいなHTMLや、他部署が適当にコピペしてきたマークアップに遭遇して絶望することがあるよね。
「タグが閉じられてない!」「`` の外に `

` がある!」――普通ならエラーでパース中断(Syntax Error)になりそうなものだが、Webブラウザは止まらない。

HTML5の仕様(Error Handling)では、「ブラウザは不正なHTMLを決してエラーにしてはならない。必ずリカバリしてDOMを構築せよ」という鉄の掟がある。

ブラウザの優しさが裏目に出る瞬間

例えば、次のような「閉じ忘れ」のHTMLがあったとする。

段落1のテキスト

ここにブロック要素がいきなり来た!

段落2のテキスト

W3Cの仕様に基づき、ブラウザのパーサは次のように暗黙的な補完(Implied End Tags)を行う。
1. `

` の中に `

` が入ることは仕様上許されない(フローコンテンツの制約)。
2. そのため、パーサは「おっと、ここで前の `

` が暗黙的に閉じられたな」と勝手に判断し、仮想的な `

` を挿入する。
3. その結果、DOMツリー構造は次のように歪められる。

  • P
  • #text: “段落1のテキスト”
  • DIV
  • #text: “ここにブロック要素がいきなり来た!”
  • P
  • #text: “段落2のテキスト”

「ブラウザが勝手に直してくれるからラッキー!」と思ってはいけない。この「ブラウザの勝手な推測(Fuzzy Parsing)」こそが、環境によってレイアウトが崩れたり、DOM操作系ライブラリ(ReactやVueなどのVirtual DOM差分アルゴリズム)が「あれ、思ってたツリーと違うぞ?」とバグる最大の原因になる。

—

3. 実践:JavaScriptでDOM構築の挙動をハックする

百聞は一見にしかず。ブラウザがいかにシビアに、そしてダイナミックにDOMを組み立てているかを、現代のフロントエンドの現場で使えるきれいなコードで体感してみよう。

以下のコードは、外部の怪しいHTMLスニペットを安全に(かつDOM構築のライフサイクルを意識しながら)パースし、親しみやすい安全なノードとして現在のドキュメントにマウントする実践的なユーティリティだ。

/

  • 与えたHTML文字列を安全にパースし、DOMノードのフラグメントとして返却する
  • @param {string} htmlString – パース対象のHTML文字列
  • @returns {DocumentFragment} 構築されたDOMフラグメント

/
export function createSafeDOMFragment(htmlString) {
// 1. DOMParser APIのインスタンスを生成
// これにより、現在のメインDOMを汚さずに独立したパーサコンテキストでHTMLを解析できる
const parser = new DOMParser();

// 2. parseFromStringでHTMLをパース
// 第2引数に ‘text/html’ を指定することで、HTML5の寛大なエラーハンドリング仕様が適用される
const doc = parser.parseFromString(htmlString, ‘text/html’);

// 3. パース時に発生した致命的なエラー(構文解析エラーなど)をチェック
// DOMParserはパースエラーがある場合、 要素をドキュメント内に生成する仕様
const parserError = doc.querySelector(‘parsererror’);
if (parserError) {
console.warn(
‘[DOM Parser Warning] HTMLの解析中に不正な構文が検出されましたが、ブラウザによりリカバリされました。’,
parserError.textContent
);
}

// 4. DocumentFragmentを使って、一括してDOMツリーを構築・アタッチする準備をする
// メモリ上でツリーを組み立てることで、余計なReflow(再レイアウト)を防ぐのがプロの技だ
const fragment = document.createDocumentFragment();

// body要素配下に構築されたDOMノードをフラグメントへ移し替える
// childNodesを直接ループしてmoveさせる
while (doc.body.firstChild) {
fragment.appendChild(doc.body.firstChild);
}

return fragment;
}

// — 【使用例】 —
// 不正なタグが含まれているHTMLをあえて渡してみる
const rawHtml = `

タイトルです ここから段落ですがタグが閉じられていません。 インライン要素がはみ出しています。 `; // 実務のコンポーネント内での処理を想定 const container = document.getElementById(‘app-container’); if (container) { const safeFragment = createSafeDOMFragment(rawHtml); // 一発でDOMに反映(ブラウザの最適化が最も効く瞬間) container.appendChild(safeFragment); console.log(‘DOMツリーの構築が完了し、無事にマウントされました。’); } シニアからのTips:なぜ `DOMParser` を使うべきなのか?

よくあるアンチパターンとして、`element.innerHTML = dirtyHtml` とやってしまうケースがある。これだと、
1. 不必要な既存DOMの破棄と再構築コストが発生する。
2. もし `dirtyHtml` に悪意のあるスクリプトが含まれていた場合、XSS(クロスサイト・スクリプティング)の脆弱性に直結する(※ブラウザは `

frontendintronationalをフォローする

コメント

タイトルとURLをコピーしました