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

やあ。今日もどこかのコンポーネントの再レンダリングループや、バンドルサイズ肥大化と格闘していることだろう。フロントエンドエンジニアとしてある程度経験を積んでくると、「なぜ画面がこう描画されるのか」というブラウザの内部挙動にぶち当たる瞬間がやってくる。

「なんだか最近、DOMの肥大化で初期表示が重い気がする……」
「HTMLの書き方一つで、パースの効率が変わるって本当?」

後輩からこんな質問を受けたとき、君は自信を持って裏側のメカニズムを解説できただろうか?
「とりあえず綺麗に書こう」では、シニアの仕事としては少し物足りない。今日は、ブラウザがサーバーから受け取ったただの「バイトの塊」を、私たちが日々触りまくっている「DOM」へと昇華させる最初の関所、「HTMLパースにおけるトークン化プロセス」の深淵を覗いていこう。

スペックシート通りの退屈な解説はしない。現場で生きるエンジニアの視点で、ブラウザの頭の中を完全にハックして見せるよ。

—

1. バイト列から文字へ:トークナイザーの「その前」の泥臭い仕事

私たちが何気なく書き飛ばす `index.html`。GitHubのサーバーからレスポンスとして送られてくるとき、あれはブラウザにとってただの「バイト列(Buffer / ArrayBuffer)」に過ぎない。文字コードすら最初は確定していない不安定な代物だ。

ブラウザのネットワーキング層からレンダリングエンジン(BlinkやWebKitなど)にこのバイト列が渡された瞬間、最初の戦いが始まる。

1. 文字エンコーディングの判定(Decoding):
HTTPヘッダーの `Content-Type: text/html; charset=UTF-8` を見たり、HTML前半の `` をスニフニング(嗅ぎ取り)したりして、バイト列を人間が読める「Unicodeの文字列」にデコードする。
2. 文字単位のストリーム入力:
デコードされた文字列は、一気にメモリ上に展開されるのではなく、ストリームとして少しずつ「トークナイザー(Tokenizer)」と呼ばれる心臓部に送り込まれる。

ここで重要なのは、「HTMLのパースは、DOMツリー構築と並行して、文字が届いた端からインクリメンタル(段階的)に行われている」という点だ。サーバーからのレスポンスがチャンク(Chunked transfer encoding)で分割送信されてくるとき、ブラウザはすべてを受け取る前であっても、届いた分からモリモリとトークン化を進めている。この泥臭いまでの最適化が、Webの初期表示スピードを支えているんだ。

—

2. トークン化(Tokenization)の核心:状態機械(State Machine)の世界

文字のストリームを受け取ったトークナイザーは、HTMLの仕様書(WHATWG Living Standard)に基づき、「有限状態機械(Finite State Machine, FSM)」として厳密に動作する。

これ、実はコンパイラ理論そのものだ。文字を1文字ずつ(あるいは数文字先読みして)スキャンしながら、現在の「状態(State)」を次々と遷移させていく。

代表的な状態の旅路

  • Data State(データ状態): 通常のテキストノードやタグの外側にいる時。`<` を見つけると、タグの始まりだと思って `Tag Open State` に遷移する。
  • Tag Open State(タグ開始状態): 次の文字が英字なら `Tag Name State` へ。`/` なら終了タグの合図なので `End Tag Open State` へ。
  • Tag Name State(タグ名状態): タグの名前(`div` や `script` など)をバッファに溜めていき、空白文字を見つければ `Before Attribute Name State` へ、`>` を見つければタグの完成として `Data State` に戻る。
  • Attribute States(属性関連の状態): 属性名、`=`、クォーテーションで囲まれた値などを緻密にパースしていく。

このFSMの何がエグいって、HTMLの「仕様の緩さ(Tag soup)」を裏で必死にカバーしている点だ。
例えば、開発者がうっかりタグを閉じ忘れたり、クォーテーションを付け忘れたりしても、トークナイザーとそれに続くツリー構築アルゴリズムが「よし、ここで勝手に補完しておこう」とエラーリカバリーを行う。あの驚異的な耐障害性は、このトークナイザーの複雑な状態遷移の賜物なのだ。

—

3. 現場で使える!ブラウザのトークン化を模した「疑似パーサー」コード

百聞は一見に如かず。ブラウザが内部で行っているトークン化のコンセプトを、TypeScriptで極めてシンプルに表現してみよう。
実際のブラウザエンジンはC++やRustで書かれ、数千行のステートマシンで爆速処理されているが、ロジックの本質はこういうことだ。

/

  • HTMLの断片を受け取り、簡易的なトークン配列に分解するトークナイザーの模倣

/
type TokenType = ‘StartTag’ | ‘EndTag’ | ‘Text’;

interface Token {
type: TokenType;
name?: string;
value?: string;
}

function mockHtmlTokenizer(htmlString: string): Token[] {
const tokens: Token[] = [];
let cursor = 0;
const length = htmlString.length;

while (cursor < length) { const char = htmlString[cursor]; if (char === '<') { // タグの開始を検知 const isEndTag = htmlString[cursor + 1] === '/'; const startIndex = isEndTag ? cursor + 2 : cursor + 1; const endIndex = htmlString.indexOf('>‘, startIndex);

if (endIndex === -1) {
// 閉じタグが見つからない場合はエラーリカバリーとしてテキスト扱いにするなど
break;
}

const tagName = htmlString.slice(startIndex, endIndex).trim();

tokens.push({
type: isEndTag ? ‘EndTag’ : ‘StartTag’,
name: tagName.split(‘ ‘)[0], // 属性部分は簡略化のため除外
});

cursor = endIndex + 1;
} else {
// テキストノードの抽出
const nextTagIndex = htmlString.indexOf(‘<', cursor); const endIndex = nextTagIndex === -1 ? length : nextTagIndex; const textContent = htmlString.slice(cursor, endIndex); if (textContent.trim().length > 0) {
tokens.push({
type: ‘Text’,
value: textContent,
});
}

cursor = endIndex;
}
}

return tokens;
}

// — 実行テスト —
const sampleHtml = `

Hello World

This is a tokenizer test.

`;
const resultTokens = mockHtmlTokenizer(sampleHtml);

console.log(‘生成されたトークン一覧:’, resultTokens);
/
出力イメージ:
[
{ type: ‘StartTag’, name: ‘div’ },
{ type: ‘StartTag’, name: ‘h1’ },
{ type: ‘Text’, value: ‘Hello World’ },
{ type: ‘EndTag’, name: ‘h1’ },
{ type: ‘StartTag’, name: ‘p’ },
{ type: ‘Text’, value: ‘This is a tokenizer test.’ },
{ type: ‘EndTag’, name: ‘p’ },
{ type: ‘EndTag’, name: ‘div’ }
]
/

このコードを実行すると、HTML文字列が「意味のある最小単位(トークン)」に綺麗に分解されるのがわかるはずだ。ブラウザは、このトークンを即座にTree Construction(ツリー構築)フェーズへ流し込み、親子関係を解決しながらDOMノードへと変換していく。

—

4. フロントエンド実務へのインパクト:なぜこの仕組みを知る必要があるのか?

「で、これが俺たちの実務にどう影響するんだ?」という話だ。知っているだけでパフォーマンスチューニングの引き出しが確実に増えるポイントを3つ挙げておこう。

1. `document.write` やインラインスクリプトが描画をブロックする理由

トークナイザーがストリームを読んでいる最中に `

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

コメント

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