フロントエンドエンジニアとして日々の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 = ‘
‘;
console.log(“— トークン化プロセス開始 —“);
// ※実際のブラウザ内部では、これの何百倍も堅牢なC++(Blinkなら)によるステートマシンが回っています。
実際のブラウザエンジン(ChromiumのBlinkなど)では、この字句解析がC++の極限まで最適化されたコードで、文字ストリームをマルチスレッド(プリローダーなど)と連携させながら超高速で処理されている。
—
4. シニアから後輩エンジニアへ伝えたいベストプラクティス
1. インラインスクリプトとスタイルシートの位置に気を配れ
HTMLパーサーは、途中で `

コメント