9月3日、Hideaki Takahashiが「Anatomy of a Browser」と題した記事を公開した。URLを入力してからピクセルが画面に描画されるまで、そしてクリックがイベントとしてコードに戻るまで——ブラウザの内部で何が起きているかを、ネットワーク層からレンダリングパイプラインまで体系的に解説した内容だ。
ブラウザは現代で最も複雑なソフトウェアの一つだ。名前解決、TLS暗号化、複数のネットワークプロトコル対応、信頼できないコードの実行、2次元ジオメトリの計算、GPU制御、ストレージ管理、互いに敵対しうるオリジン間の分離——これらすべてを数十ミリ秒以内の応答で並行してこなしている。HTTP/3の本格普及やChromiumエンジンの市場支配が進む中、Core Web Vitalsへの対応を迫られるWeb開発者がブラウザ内部を体系的に理解する必要性は高まっている。Chrome、Firefox、Safariとエンジンごとに実装は異なるが、この記事が目指すのは「すべてのブラウザエンジンが共通して解決しなければならない問題と、それをつなぐアーキテクチャの理解」だ。
ナビゲーションは「GETリクエストを送る」ではない
https://example.com/articles/browser?q=rendering#layout を入力してEnterを押す。ブラウザはまず、この入力がURLなのか検索クエリなのか、あるいは内部コマンドなのかを判断する。その後のナビゲーションは単純なHTTPリクエストではなく、以下を考慮したステートマシンとして動く。
- HSTS(HTTP Strict Transport Security)ルールによる
http→httpsの自動アップグレード - セーフブラウジングやエンタープライズポリシーによるブロック
- 既存キャッシュやService Workerの利用可否
- DNS・TLS・HTTP接続の再利用可否
ナビゲーションの結末は「新しいページを表示する」だけではない。リダイレクト追跡、ファイルダウンロード、エラーページ表示、別アプリへの委譲など、多岐にわたる。
DNSからTLSハンドシェイクまで
名前解決ではexample.comをIPアドレスにマッピングする。ブラウザ独自のDNSキャッシュ、OSのリゾルバー、設定済みDNSサーバー、DNS over HTTPS(DoH)などを順に参照する。DNSはただの辞書引きではなく、レコードの有効期限、エイリアスチェーン、プライベートアドレス空間のセキュリティ含意など、複雑な問題を内包している。
トランスポートの確立では、HTTPSの場合に主に2択が生じる。
- TCP + TLS上のHTTP/1.1またはHTTP/2
- UDP上のQUICを利用するHTTP/3(トランスポートと暗号ネゴシエーションを統合し、TCPのヘッドオブラインブロッキングを回避)
TLSハンドシェイク中、ブラウザは証明書チェーン、ホスト名、有効期間、鍵の用途、失効シグナルを検証し、ALPN(Application-Layer Protocol Negotiation)でHTTP/2などのアプリケーションプロトコルもネゴシエートする。
キャッシュは「後付けの最適化」ではなく、アルゴリズムの一部だ。ブラウザが持つキャッシュの種類は多岐にわたる。
- DNS・コネクションキャッシュ
- HTTPレスポンスキャッシュ
- デコード済み画像キャッシュ
- コンパイル済みJavaScript/WebAssemblyのコードキャッシュ
- バック/フォワードキャッシュ(bfcache)——ページ全体とJavaScriptヒープを保持し、戻る・進む操作を瞬時にする仕組み
HTMLパース:バイト列からDOMへ
レンダラーはHTMLのバイトストリームをDocumentオブジェクトへ変換する。このプロセスは3段階だ。
- バイト→文字のデコード:BOM、
Content-Typeヘッダー、<meta charset>などからエンコーディングを決定する - トークナイズ:文字列を
StartTag、Character、EndTagなどのトークンに変換する。このトークナイザーはコンテキスト依存であり、<の意味は通常テキスト、コメント、スクリプト内で異なる - ツリー構築:トークンからDOMノードを生成し、木構造を組み立てる
DOMは元のHTMLテキストと同一ではない。パーサーは不正な構造を修復し、省略された要素を補完する。<html>、<head>、<body>を省略したHTMLソースでも、DOMにはそれらが生成される。
また、ページはパース中に自分自身を書き換えられる点が重要だ。CSSはフォントや画像をロードでき、JavaScriptはネットワーク通信を起こしてDOMを変更し、レイアウトを計測してさらにDOMを変更できる。コンポジタースレッドがスクロールを処理している間、メインスレッドは別の作業をこなす。この並行性こそが複雑さの根源だ。
スタイル計算とレイアウト
CSSのカスケードでは、同一プロパティに複数の宣言が競合した場合に以下の優先順位で勝者を決める。
- メディア条件への適合性
- オリジンと重要度(
!important) - カスケードレイヤー
- セレクター詳細度(Specificity)
- スコーピング近接度
- ソース順
CSSの値は「declared → cascaded → specified → computed → used → actual」という段階を経る。width: 50%はcomputed styleではパーセンテージのまま残り、レイアウト時に包含ブロックのサイズが判明して初めて絶対値のused widthになる。
セレクターマッチングは右から左へ評価するのが一般的だ。main .card > a:hoverというセレクターに対しては、まずホバー状態の<a>を起点に、親が.cardクラスを持つか確認し、その先祖にmainがあるかを探す。すべてのmain要素の子孫を走査するより効率的な戦略だ。
レイアウトでは、スタイルが決まった後にボックスのサイズと位置を計算する。「このブロックの幅は?」「この行はどこで折り返す?」「フレックスコンテナの空きスペースはどう配分する?」といった問いに答える段階だ。
処理の全体像
記事では最終的な処理フローを次のように整理している。Browser UIがナビゲーションと権限管理を担い、ネットワークスタックとキャッシュを経てDocument Loaderに渡る。ここからHTMLパーサーがDOM、CSSパーサーがスタイルルール、JavaScriptエンジンがWeb APIと連携しながら処理を進める。最終的にはcomputed style → layout → paint/display list → ラスタライズ/コンポジット → ピクセルという流れでGPUに出力される。
実際のブラウザではこれらの処理はプロセスとスレッドに分散されており、各ステージが前のステージの完了を待つわけでもない。HTMLは受信しながら同時にパースされ、サブリソースは早期に発見され、コンポジターは古いラスタライズ済みコンテンツを再利用できる。
今回紹介した内容に加え、元記事ではJavaScript実行エンジン、イベントループ、クリックからDOMイベントへの変換、プロセス・スレッドモデル、セキュリティ、ストレージ、ページ変更時の差分処理など、さらに深い層まで解説が続く。ブラウザの動作原理を体系的に学びたい読者には、MDNのブラウザ動作解説やChrome開発者ブログも併せて参照されたい。
詳細はAnatomy of a Browserを参照していただきたい。