こんにちは。ブラウザのレンダリングパイプラインを覗き見ることが、毎晩の酒のつまみになるような奇特なエンジニアの皆さん。
今日は、私たちが普段何気なく書いているHTMLという名のテキストの塊が、BlinkやWebKitといったブラウザエンジンのコアでどのように「咀嚼」され、メモリ上に構築されているのか。その最下層、字句解析の心臓部である「HTMLパーサーのトークン化プロセス(Tokenization)」について、実務の現場で役立つアーキテクチャの視点から徹底的に解剖していこうと思う。
「たかがHTMLパースでしょ?V8のJIT最適化やReactのReconciliationに比べたら地味じゃないか」と思ったそこのあなた。甘い。このトークン化のメカニズムとメモリ効率の相関関係を理解していないと、大規模なSPAの初期表示速度(LCP)やメモリプレッシャーの最適化で痛い目を見ることになる。
ブラウザがバイト列からDOMを生み出すまでの泥臭い舞台裏へ、少し深くまで潜ってみよう。
—
1. 文字列から意味へ:トークン化という名の「極限の字句解析」
ブラウザがネットワーク経由で受け取るデータは、突き詰めればただの「UTF-8などでエンコードされた連続したバイト列」に過ぎない。これを画面上のインタラクティブなオブジェクトに変える最初の関所が、HTMLパーサーであり、その中でも最もCPUサイクルを消費するフェーズがトークン化(Tokenization)だ。
一般的なプログラミング言語のコンパイラ(例えばTypeScriptなど)であれば、字句解析(Lexer)と構文解析(Parser)は綺麗に分離されている。しかし、HTMLの仕様(WHATWG Living Standard)が規定するパーサーは、そんなお上品な代物ではない。HTMLは「タグの閉じ忘れ」「不正なネスト」「壊れた属性値」といった、人類の無秩序なマークアップの歴史そのものを救済しなければならないため、状態機械(State Machine)をベースにした、極めてアグレッシブかつ泥臭いストリーミング・パーサーとして実装されている。
状態機械(State Machine)と文字単位のステート遷移
HTMLトークン化の基本概念は「ステートマシン」だ。入力ストリームから1文字ずつ(あるいはチャンク単位で)読み込み、現在の「状態(State)」に応じて次のアクションと状態を決定する。
例えば、以下のような基本的なHTML断片があったとする。
パーサーの脳内(C++レベルの内部実装)では、以下のようなステートの旅が行われている。
1. Data State(データ状態): `<` に遭遇するまで、文字をそのまま文字参照やテキストノードのバッファに突っ込み続ける。
2. Tag Open State(タグ開始状態): `<` を検知して遷移。次が文字であれば Tag Name State(タグ名状態) へ。
3. Tag Name State: `div` という文字を読み取り、`div` というタグ名の「Start Tag Token」を生成し始める。
4. Before Attribute Name State(属性名開始前状態): スペースを検知し、属性の読み取りに備える。
5. Attribute Name State / Attribute Value State: `class` という属性名と `container` という属性値を結びつけ、トークンにアタッチする。
6. Self-closing start tag / Data State: `>` を検知してタグの閉じを確定し、再び Data State に戻って “Hello World” をテキストトークンとして切り出す。
この一連の処理が、数メガバイトあるHTMLドキュメントに対して、ストリーミングで行われている。ネットワークからTCPパケットが到着するたびに、ブラウザはこのステートマシンを高速で回し続けているのだ。
—
2. メモリ効率とGCプレッシャー:知られざるアロケーションの戦い
フロントエンドエンジニアが意識するメモリリークといえば、SPAでのイベントリスナーの解除忘れや、クロージャによるDOM参照の保持が定番だ。しかし、「HTMLの書き方そのものが、パーサーのメモリ効率とGC(ガベージコレクション)プレッシャーに直結している」という事実を意識したことはあるだろうか?
BlinkやWebKitなどのモダンブラウザエンジンは、トークン化の過程で膨大な数のオブジェクト(Token構造体やStringImplなど)を一時的にアロケートしている。
巨大なインラインスクリプトと `` タグの罠
ここで、実務におけるパフォーマンス最適化の観点から非常に重要なポイントがある。それは「Script-inclusive parsing(スクリプト内包パース)」の挙動だ。
パーサーがトークン化を進めている最中に `
ブラウザのHTMLパーサーは、この巨大なテキストブロックを通常のテキストノードやトークンとして処理する過程で、内部の文字列バッファを急速に拡大させる。
さらに最悪なのは、これがメインスレッドのヒープメモリを直接圧迫する点だ。V8などのJSエンジンは、HTMLパーサーとメモリ空間(Heap)を共有、あるいは密接に連携しているため、巨大なHTML構造のパースは、そのままJSのGC発火リスクを高めることになる。
【上級エンジニア向け対策:ペイロードの外部化と非同期化】
- 巨大な初期データはHTML内にインラインで埋め込むべきではない。
- 必ず別ファイルのJSONとして配信し、`fetch()` を用いて非同期で取得するべきだ。これにより、HTMLパーサーのステートマシンが巨大な文字列データ処理でブロックされるのを防ぎ、DOMツリー構築のパイプラインをクリーンに保つことができる。
---
3. 非同期の競合とレンダリングブロック:Speculative Parserの限界
「HTMLは上から下へ順番に読まれる」というのは、入門書の嘘ではないが、モダンブラウザの最適化を語る上ではあまりにもナイーブな認識だ。
現代のブラウザは、メインのHTMLパーサーがネットワーク遅延や重い同期スクリプトでブロックされたときのために、「Speculative Parser(投機的パーサー)」という裏の仕組みを走らせている。メインのパーサーがJSの実行を待っている間に、先読みスレッドがHTMLストリームを先回りしてスキャンし、``, `

コメント