- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに — 1秒に満たない時間で繰り広げられる大叙事詩
- ステップ1 — URLのパースと前処理
- ステップ2 — DNS解決:名前をアドレスへ
- ステップ3 — TCPの3ウェイハンドシェイク:接続を確立する
- ステップ4 — TLSハンドシェイク:安全な通り道を作る
- ステップ5 — HTTPリクエストを送る
- ステップ6 — サーバーの処理とレスポンス
- ステップ7 — HTMLのパースとDOMの構築
- ステップ8 — クリティカルレンダリングパス:ピクセルまで
- 全体の流れをもう一度
- おわりに
- 参考資料
はじめに — 1秒に満たない時間で繰り広げられる大叙事詩
「アドレスバーにURLを入力してEnterを押すと何が起きますか?」これは面接の定番質問ですが、定番であり続けるだけの理由があります。この一行の操作の中に、Webというシステムのほぼすべての層が凝縮されているからです。DNS、ルーティング、トランスポート層、暗号化、アプリケーションプロトコル、そしてレンダリングエンジン。どれか一つをきちんと説明しようとすると、すべてを少しずつ知っている必要があります。
この記事は、その旅路を順番にたどります。ブラウザがURLを解釈する瞬間から、画面に最初のピクセルが描かれる瞬間まで、各段階で実際に何が行き交うのかを見ていきます。例として使うアドレスは https://example.com/store/cart?id=42 とします。
ステップ1 — URLのパースと前処理
Enterを押した直後、ブラウザはまずアドレスバーの文字列が何なのかを判断します。これはURLなのか、それとも検索語なのか。スペースがあったりドット(.)がなかったりすればデフォルトの検索エンジンに渡し、URLらしければパースを始めます。
URLはいくつかの部分に分かれます。
https://example.com:443/store/cart?id=42#top
└─┬─┘ └────┬────┘└┬┘└───┬────┘└──┬──┘└┬┘
スキーム ホスト ポート パス クエリ フラグメント
- スキーム(scheme):
https。どのプロトコルで通信するかを決めます。 - ホスト(host):
example.com。接続先サーバーのドメイン名です。 - ポート(port): 省略すると
httpsは443、httpは80が既定です。 - パス(path)、クエリ(query)、フラグメント(fragment): サーバー内のどの資源が欲しいか、どんなパラメータを付けるか、文書内のどこへスクロールするかを表します。
この段階でブラウザはすでにいくつかの最適化を行います。HSTS(HTTP Strict Transport Security)のリストにあるドメインなら、http で入力しても強制的に https に書き換えます。さらにブラウザキャッシュ、サービスワーカー、HTTPキャッシュにこの資源があるかも確認し、あればネットワークを丸ごとスキップすることもあります。
ステップ2 — DNS解決:名前をアドレスへ
ホスト名 example.com は人間のためのものです。ネットワークはIPアドレスで通信するので、ドメイン名をIPアドレスに変換する過程が必要になります。これがDNS(Domain Name System)解決です。
ブラウザは複数の層のキャッシュを順番にたどります。どこかの層で答えが見つかればその時点で終わるので、実際の問い合わせはすべてのキャッシュが空のときだけ発生します。
- ブラウザ自身のDNSキャッシュを見ます。
- なければOSのリゾルバキャッシュ(と
hostsファイル)を見ます。 - それでもなければ、設定された再帰リゾルバ(recursive resolver)、通常はISPや公開DNS(例:8.8.8.8)に尋ねます。
再帰リゾルバも答えを知らなければ、いよいよ本当の再帰問い合わせが始まります。リゾルバは複数の権威サーバーを順に訪ねます。
再帰リゾルバ
│ 1) 「example.com のIPは?」
▼
ルートサーバー -> 「.com 担当はあちらのTLDサーバーだよ」
│
▼
.com TLDサーバー -> 「example.com 担当のネームサーバーはここだよ」
│
▼
権威ネームサーバー -> 「example.com は 93.184.216.34 だよ」
│
▼
リゾルバが結果をキャッシュしてブラウザに返す
各応答には**TTL(Time To Live)**が付いています。TTLの間はリゾルバがその答えをキャッシュに保持するので、同じドメインを再び尋ねてもルートまで行かずに速く答えられます。DNSが概して速く感じられるのは、この多層キャッシュのおかげです。
パフォーマンスのため、ブラウザはページ読み込みの前にあらかじめ名前を解決しておくDNSプリフェッチも行います。リンクにマウスを乗せた瞬間、裏で静かに解決を始めておくといった具合です。
ステップ3 — TCPの3ウェイハンドシェイク:接続を確立する
IPアドレスが分かったので、次はそのサーバーと接続を結ぶ番です。https は信頼性のあるトランスポート層であるTCPの上で動くので、データを送る前にまずTCP接続を確立します。これが有名な3ウェイハンドシェイクです。
クライアント サーバー
│ ── SYN (seq=x) ─────────────►│ 「接続したい」
│ │
│ ◄──── SYN-ACK (seq=y, ack=x+1)│ 「いいよ、こちらも準備できた」
│ │
│ ── ACK (ack=y+1) ───────────►│ 「確認、始めよう」
▼ ▼
これで双方向のデータ転送が可能になる
3つのメッセージのうち2つが行き交った後から、実質的に接続が成立します。各段階でシーケンス番号(seq)を交換しますが、これは以降のデータの順序を合わせ、欠落を検知する基準になります。このハンドシェイクは最低でも1 RTT(round-trip time、往復時間)を消費します。サーバーが地球の裏側にあれば、この一度の往復だけで数百ミリ秒かかることもあります。
参考までに、最新のHTTP/3はTCPの代わりにUDPベースのQUICを使い、この接続確立と次の段階である暗号化を一つにまとめて遅延を減らします。ただ概念を理解するには従来のTCP + TLSの組み合わせが最も明快なので、この記事はその流れをたどります。
ステップ4 — TLSハンドシェイク:安全な通り道を作る
TCP接続は立ちましたが、その上を流れるデータはまだ平文です。https の 's' はTLS(Transport Layer Security)を意味し、実際のHTTPデータを送る前に暗号化された通り道をまず作ります。TLSハンドシェイクがすることは大きく三つ。サーバーが本当にそのサーバーかを認証し、通信を暗号化する鍵を交換し、使う暗号方式をネゴシエーションします。
TLS 1.3を基準に流れを単純化するとこうなります。
クライアント サーバー
│ ── ClientHello ────────────────►│ 対応する暗号スイート、鍵共有の一覧
│ │
│ ◄── ServerHello, Certificate ───│ 選んだ暗号、証明書、鍵共有
│ + Finished │
│ │
│ ── Finished ───────────────────►│ 検証完了
▼ ▼
以降すべてのHTTPデータは暗号化されて流れる
主要な段階をほぐしてみます。
- 証明書の検証: サーバーは自身の証明書を送ります。ブラウザはこの証明書が信頼された認証局(CA)の署名を受けているか、ドメイン名が一致するか、期限切れでないかを確認します。一つでも食い違えば、あの恐ろしい「安全でない接続」の警告が出ます。
- 鍵交換: 双方は公開鍵暗号(例:ECDHE)を使い、盗聴者が行き交うメッセージをすべて見ても割り出せない共有秘密を作り出します。この秘密から、実際のデータを暗号化する対称鍵が導出されます。
- 暗号スイートのネゴシエーション: どの暗号化・ハッシュアルゴリズムの組み合わせを使うかを決めます。
TLS 1.3はこの全体を1 RTTに縮め、以前訪れたサーバーなら0-RTTでの再開まで可能です。旧バージョンのTLS 1.2が2 RTTを使っていたのと比べると大きな改善です。証明書の検証、暗号スイート、ハンドシェイクの流れを自分の目で実験してみたければ、このサイトの認証・セキュリティ実験室でTLSと関連概念を触ってみられます。
ステップ5 — HTTPリクエストを送る
暗号化された通り道が完成したので、いよいよブラウザが求めていたものをリクエストする番です。ブラウザはHTTPリクエストメッセージを作って送ります。HTTP/1.1を基準にすると、リクエストはおおよそこんなテキストの形です。
GET /store/cart?id=42 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, br
Cookie: session=abc123
Connection: keep-alive
各行の意味を押さえましょう。
- リクエストライン: メソッド(
GET)、パス(/store/cart?id=42)、プロトコルバージョン。 - Hostヘッダー: 一つのIPに複数のサイトが載っている場合(バーチャルホスティング)、どのドメインが欲しいかを伝えます。
- Accept系ヘッダー: どんなコンテンツ形式と圧縮を受け取れるかをサーバーに知らせます。
- Cookieヘッダー: 以前サーバーが埋め込んだクッキーを送り返し、ログイン状態などを維持します。
HTTP/2やHTTP/3では、これらのヘッダーは人間が読むテキストではなくバイナリフレームに圧縮されて転送され、一つの接続で複数のリクエストを同時に(多重化)送れます。ですが論理的に含まれる情報は上と同じです。
ステップ6 — サーバーの処理とレスポンス
リクエストはネットワークを越えてサーバーに届きます。ところが「サーバー」はたいてい単一の機械ではありません。リクエストは通常、複数の層を経由します。
リクエスト --> [CDN / エッジキャッシュ] --> [ロードバランサ] --> [リバースプロキシ]
│(キャッシュヒットならここで即応答)
--> [Web/アプリサーバー] --> [データベース / キャッシュ]
- CDNとエッジキャッシュ: 静的資源(画像、CSS、JS)やキャッシュ可能なページは、ユーザーに近いエッジサーバーが即座に応答します。そうすればオリジンまで行く必要がなく、ずっと速くなります。
- ロードバランサ: トラフィックを複数のサーバーインスタンスに分散します。
- アプリケーションサーバー: 実際のロジックを実行します。ルーティングをし、データベースからデータを読み、HTMLをレンダリングしたりJSONを作ったりします。
処理が終わると、サーバーはHTTPレスポンスを返します。レスポンスの最初の行にはステータスコードが載ります。
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Encoding: br
Cache-Control: max-age=3600
Set-Cookie: session=abc123; HttpOnly; Secure
<!DOCTYPE html>
<html> ... </html>
ステータスコードは結果を三桁の数字で要約します。200 は成功、301・302 はリダイレクト、404 は資源なし、500 はサーバーエラーです。これらの数字の正確な意味やニュアンスが気になれば、HTTPステータスコード図鑑で各コードを一つずつ見ていけます。レスポンス本文はたいてい gzip や br で圧縮されて届くので、ブラウザはまず展開する必要があります。
ステップ7 — HTMLのパースとDOMの構築
ブラウザはHTMLのバイトを受け取り始めると、最後まで全部受け取るのを待たず、届くそばからパースを始めます。パースの目標は、文書をツリー構造である**DOM(Document Object Model)**に変えることです。
HTMLバイト --> トークン化 --> ノード生成 --> DOMツリー
パースの途中でブラウザは、追加で必要な資源に出会います。<link> で参照されるCSS、<script> で参照されるJavaScript、<img> の画像などです。ここで重要な性質が二つあります。
- CSSはレンダリングを止める(render-blocking): ブラウザはスタイルをすべて把握する前に画面を安全に描けません。そのためCSSを取得してパースし、**CSSOM(CSS Object Model)**を作るまでレンダリングを先送りします。
- スクリプトはパースを止めうる(parser-blocking): 普通の
<script>は出会った瞬間にHTMLパースを止めて、ダウンロードして実行します。スクリプトがDOMを変えうるからです。これを避けるにはasyncやdeferを付けて、パースと並行させたり後回しにしたりします。
それでもブラウザはプリロードスキャナを走らせ、本格的にパースが到達する前に、これから必要になる資源を先にダウンロードし始めます。おかげでスクリプトがパースを止めている間も、他の資源のダウンロードは続きます。
ステップ8 — クリティカルレンダリングパス:ピクセルまで
DOMとCSSOMが揃うと、ブラウザはこの二つを組み合わせて画面を描く一連の段階を経ます。この全過程を**クリティカルレンダリングパス(Critical Rendering Path)**と呼びます。
DOM + CSSOM --> レンダーツリー --> レイアウト --> ペイント --> 合成
各段階がすることはこうです。
- レンダーツリー(Render Tree): DOMとCSSOMを結合しますが、実際に画面に見えるノードだけを含めます。
display: noneの要素はここで外れます。 - レイアウト(Layout / Reflow): 各要素が画面のどこに、どれだけの大きさで置かれるか、幾何学的な位置を計算します。ビューポートのサイズが変わると、この計算をやり直す必要があります。
- ペイント(Paint): 各要素のピクセルを実際に塗ります。色、テキスト、画像、影などを埋めます。
- 合成(Composite): 複数のレイヤーに分かれたペイント結果を正しい順序で合わせ、最終画面を作ります。GPUがこの処理を加速します。
ここでパフォーマンスに直結する概念が**リフロー(reflow)とリペイント(repaint)**です。JavaScriptで要素のサイズや位置を変えるとレイアウトから計算し直すリフローが起き、色だけ変えるとレイアウトには触れず塗り直すリペイントだけが起きます。リフローはリペイントより高価なので、滑らかなアニメーションのためにはレイアウトを引き起こさないプロパティ(transform、opacity など)を使うのが有利です。これらのプロパティは合成段階だけで処理され、レイアウトとペイントを飛ばせるからです。
全体の流れをもう一度
ここまでの旅路を一目で整理するとこうなります。
1. URLパース 入力文字列を スキーム/ホスト/パス に分解
2. DNS解決 ドメイン名 -> IP (多層キャッシュ + 再帰問い合わせ)
3. TCPハンドシェイク 3ウェイで信頼性のある接続を確立 (1 RTT)
4. TLSハンドシェイク 認証 + 鍵交換 + 暗号ネゴシエーション (1 RTT)
5. HTTPリクエスト メソッド/ヘッダー/クッキーを載せて送信
6. サーバー処理 CDN/LB/アプリサーバー/DB を経てレスポンス生成
7. HTMLパース バイト -> DOM、CSS -> CSSOM
8. レンダリング レンダーツリー -> レイアウト -> ペイント -> 合成
各段階はそれ自体が本一冊分の深さを持っていますが、大きな絵で見れば、結局は一つの目標に向かっています。「人が読めるアドレス」を「画面上のピクセル」に変えることです。
おわりに
アドレスバーにURLを入力してEnterを押す、その短い瞬間に、名前解決、接続確立、身元認証、暗号化、データ転送、文書パース、画面レンダリングという、まったく性格の異なる作業が正確な順序で噛み合って動きます。この流れを理解すれば、パフォーマンスの問題に出会ったときにどの段階を疑えばよいか、セキュリティ警告が出たときに何が食い違ったのかを、はるかに速くつかめます。
次にページの読み込みが遅く感じたら、この八つの段階のどこで時間が漏れているか想像してみてください。DNSか、ハンドシェイクか、サーバー処理か、それともレンダリングか。その問いを立てられるだけで、すでにWebを一段深く見られるようになっています。
参考資料
- MDN: How browsers work — https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work
- MDN: Populating the page — how browsers work — https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work
- Cloudflare Learning: What is DNS? — https://www.cloudflare.com/learning/dns/what-is-dns/
- Cloudflare Learning: What happens in a TLS handshake? — https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/
- web.dev: Critical rendering path — https://web.dev/articles/critical-rendering-path
- High Performance Browser Networking (Ilya Grigorik) — https://hpbn.co/