【実務・中級編】 HTMLパーサーのトークン化プロセス – Webブラウザの仕組み実践ガイド

フロントエンドエンジニアとして日々のUI構築やパフォーマンスチューニングに明け暮れていると、どうしても「JSの実行速度」や「Reactの再レンダリング」といった上位レイヤーに目が行きがちだ。

だが、ちょっと待ってほしい。ブラウザが画面を表示するその瞬間、バックグラウンドでは一体何が行われているのか? サーバーから送られてきたただの「文字列」であるHTMLが、どうやってピクセルに変換されているのか。その最下層の泥臭いメカニズムを知らずして、「真に速いWebアプリケーション」は作れない。

今回は、HTMLパースの第一歩であり、ブラウザの心臓部とも言える「HTMLパーサーのトークン化プロセス」について、実務の現場目線で深く掘り下げていこう。

—

1. なぜ「ただの文字列」がDOMになるのか?

私たちは普段、VS Codeなどのエディタで何気なくHTMLを書いている。

こんにちは、世界

ブラウザから見れば、これはただの `UTF-8` の文字のストリーム(データの流れ)に過ぎない。これをメモリ上のオブジェクト(DOMツリー)に変換するためには、いくつかの厳格なステップを踏む必要がある。

その最初の関門が、字句解析(Lexical Analysis)、すなわち「トークン化(Tokenization)」だ。

文字列から「意味の最小単位(トークン)」への分解

正規表現や単純な文字列置換でHTMLをパースしようとしたことがあるダークな過去を持つエンジニアなら知っているはずだ。「HTMLは正規表現ではパースできない」。なぜなら、HTMLの仕様(WHATWGのHTML Living Standard)は、極めて柔軟(あるいは狂気的)なエラー回復アルゴリズムを内包しているからだ。

ブラウザのパーサーは、文字ストリームを1文字ずつ(あるいはチャンク単位で)読み込み、「状態機械(State Machine)」を用いてトークンへと切り分けていく。

トークンの種類は主に以下の6つに分類される。
1. DOCTYPE (``)
2. StartTag (`

`)
3. EndTag (`

`)
4. Comment (``)
5. Character (`こんにちは、世界`)
6. EndOfFile (ファイルの終端)

—

2. 状態機械(State Machine)の内部世界

トークン化のプロセスを支えているのが、「トークナイザー(Tokenizer)」だ。これは有限状態機械(Finite State Machine)として実装されている。

文字を1文字ずつ舐めながら、現在の「状態(State)」に応じて次の状態へと遷移していく。

  • Data State(データ状態): 通常のテキストやタグの開始を待つ基本の状態。`<` が来ると Tag Open State へ遷移する。
  • Tag Open State(タグ開始状態): `/` が来れば End Tag Open State、アルファベットなら TagName State へ遷移。
  • TagName State(タグ名状態): タグ名をバッファリングし、スペースが来れば Before Attribute Name State へ、`>` が来ればタグの完成として Data State へ戻る。

この状態機械の何がエグいって、「人間が書き散らかした不正なHTML」をいかに美しく(あるいは仕様通りに)解釈するかという執念の塊なのだ。

現場で役立つ教訓:ブラウザの「優しさ」に甘えるな

WHATWGの仕様書には、タグが閉じられていなかったり、アトリビュートのクォーテーションが抜けていたりした場合の「エラー回復(Error Recovery)アルゴリズム」が細かく規定されている。

例えば、以下のようなクソコードを書いても、ブラウザは怒らずに画面を表示してくれる。

太字の段落

「お、ちゃんと閉じてないから、裏で勝手に補完しといたぜ!」というのがブラウザの親切心(エラー回復)だ。しかし、このパースとエラー回復の処理コストは、確実にレンダリングの遅延(TBTの悪化)につながる。

パーサーが「あれ、これどう解釈すりゃいいんだ……?」と迷う瞬間、メインスレッドはその解決にCPUサイクルを奪われている。きれいなHTMLを書くことは、SEOやアクセシビリティのためだけでなく、ブラウザのパーサーを無駄に悩ませないための優しさなのだ。

—

3. 実践:JavaScriptで「ミニ・トークナイザー」を書いてみる

「ブラウザが裏で何をやっているか」を一番手っ取り早く理解する方法は、自分でその縮小版を書くことだ。

以下のコードは、非常にシンプルなHTML文字列を文字単位で走査し、簡易的なトークン(開始タグとテキスト)に分解するミニ・トークナイザーだ。
手元のエディタに貼り付けて、Node.jsやブラウザのコンソールで動かしてみてほしい。

/

  • 超簡易版HTMLトークナイザー(学習用)
  • @param {string} htmlString – パース対象のHTML文字列

/
function simpleHtmlTokenizer(htmlString) {
let state = ‘DATA’; // 初期状態
let i = 0;
const tokens = [];

let currentToken = null;
let currentAttribute = { name: ”, value: ” };

while (i < htmlString.length) { const char = htmlString[i]; switch (state) { case 'DATA': if (char === '<') { // テキストトークンが溜まっていれば確定してプッシュ state = 'TAG_OPEN'; } else { // テキスト文字の収集(実際はCharacter Tokenとして扱う) process.stdout.write(`[Char] ${char}\n`); } break; case 'TAG_OPEN': if (char === '/') { state = 'END_TAG_OPEN'; } else if (/[a-zA-Z]/.test(char)) { currentToken = { type: 'StartTag', tagName: char, attributes: {} }; state = 'TAG_NAME'; } break; case 'TAG_NAME': if (char === '>‘) {
tokens.push(currentToken);
currentToken = null;
state = ‘DATA’;
} else if (/\s/.test(char)) {
state = ‘BEFORE_ATTR_NAME’;
} else {
currentToken.tagName += char;
}
break;

// ※実務の仕様はここから何十倍も複雑になりますが、基本構造は同じです
}
i++;
}

return tokens;
}

// 動作確認用のサンプルコード
const sampleHtml = ‘

Hello

‘;
console.log(“— トークン化プロセス開始 —“);
// ※実際のブラウザ内部では、これの何百倍も堅牢なC++(Blinkなら)によるステートマシンが回っています。

実際のブラウザエンジン(ChromiumのBlinkなど)では、この字句解析がC++の極限まで最適化されたコードで、文字ストリームをマルチスレッド(プリローダーなど)と連携させながら超高速で処理されている。

—

4. シニアから後輩エンジニアへ伝えたいベストプラクティス

1. インラインスクリプトとスタイルシートの位置に気を配れ
HTMLパーサーは、途中で `

シェアする
frontendintronationalをフォローする

コメント

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