Web
Blink

実際のところ「ブラウザを立ち上げてページが表示されるまで」には何が起きるのか

9月15日(土) DNSについて追記しました。バックエンド?今はクラウドがきっと上手くやってくれるので深く考える必要は無いんですよ(知らないので書けません😔)

問題のツイート

解釈

今回は「ChromeのURL欄に入力してからページが表示されるまで」をやります。ブラウザの起動云々はWeb系の話じゃないと信じてます。

1. HTTPリクエストが飛ぶ

HTTP2のヘッダ圧縮技術に全て書いてありました。
(追記)下のリクエストヘッダはテキストで書かれていますが、実際にはこれをバイナリにしたものが飛んでいるとのことです。segfoさんありがとうございます!

更にいうと、TLSセッションの確立についても抜けてますし、ALPNの話も、http/2上でのネゴシエーション(サーバプッシュどうするかとか)の話がないのでhttp/2前提であれば少々どころではなく相当抜けがあるかと思います。

らしいです。この辺りは僕の知識がなくて色々抜けているのでこの記事だけを読んで分かったつもりにならないよう気をつけましょう。

実際のリクエストヘッダ
:authority: www.google.co.jp
:method: GET
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: ja,en-US;q=0.9,en;q=0.8
cache-control: max-age=0
cookie: 🤫
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36
x-client-data: CJW2yQEIpbbJAQjBtskBCKmdygEI2J3KAQjancoBCKijygEIh6fKAQ==

今回はリクエストメソッドがGETなのでヘッダのみですが、POSTやPUTの場合はデータがリクエストボディで送られます。詳しくはHTTPメソッド(CRUD)についてまとめたなどを参照してください。

余談ですが、何年か前に【パズル1】ほとんどのエンジニアには解けるが、下位10%のダメなエンジニアにだけ解けないパズル?で遊んだのを思い出しました。楽しくHTTPメソッドについて学べるパズルなので解いていない人はぜひ解いてみてください。

実際のレスポンスヘッダ
HTTP/1.1 200
status: 200
date: Tue, 11 Sep 2018 09:36:30 GMT
expires: -1
cache-control: private, max-age=0
content-type: text/html; charset=UTF-8
strict-transport-security: max-age=3600
content-encoding: br
server: gws
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
set-cookie: 1P_JAR=2018-09-11-09; expires=Thu, 11-Oct-2018 09:36:30 GMT; path=/; domain=.google.co.jp
alt-svc: quic=":443"; ma=2592000; v="44,43,39,35"

レスポンスヘッダは上みたいな感じで、このヘッダと一緒に届くレスポンスボディが<!doctype html>〜〜というhtmlドキュメントです。

もっと低レイヤーについてはOSI参照モデルまとめが分かりやすいです。

(追記)HTTPリクエストからレスポンスまでを飛ばし過ぎという声が多かったのでもうちょっと書きます。

DNS

DNS?何ですかそれは。

DNSプロトコルってよく分かりませんしHTTPSで名前解決できたら楽でいいですよね。

(追記)なんかはてブとかtwitterとか見てるとDNSに興味津々の方が多いようだったので詳しく調べてみました。

DNSプロトコル

実際のDNSクエリ例
0020   xx xx xx xx xx xx xx xx xx xx c4 db 01 00 00 01
0030   00 00 00 00 00 00 03 77 77 77 06 67 6f 6f 67 6c
0040   65 02 63 6f 02 6a 70 00 00 01 00 01

c4 dbがトランザクションIDです。どのクエリとどの応答が一致しているかを確かめるために使用します。
次の01 00は2bitで表すと0000 0001 0000 0000となります。まず前半の8bitを見ていきます。

  • 0... .... 先頭の1bitはクエリなら0、レスポンスなら1となるフラグです。
  • .000 0... 次の4bitはオペレーションコードです。0は通常のクエリです。
  • .... .0.. 管理権限フラグだそうです。DNSが権威サーバーかどうかについてを表しており、リクエストでは意味のないbitです。
  • .... ..0. いにしえのDNSは1パケット512byteまでしか扱えなかったらしく、メッセージが512byteを超えた場合には途中で切り捨てられていることを示す必要があったようです。切り捨てられた場合1になります。
  • .... ...1 フルリゾルブを要求するbitです。

後半は全て0ですが、ほとんどがレスポンスでしか意味のないbitのようです。クエリで意味のあるのは.Z.C ....の2箇所で、Zは予備bit、CはDNSSECを禁止する要求らしいです。

その後00 01 00 00 00 00 00 00と0が続きますが、前から2byteずつ「Questionセクション」、「Answerセクション」、「Authorityセクション」、「Additionalセクション」の数を表しています。今回はQuestionセクションが1つ(www.google.co.jp)なので最初だけ1であとは全部0です。

残りの部分がQuestionセクションです。
03 77 77 77 06 67 6f 6f 67 6c 65 02 63 6f 02 6a 70 00 00 01 00 01

  • 03 文字数
  • 77 77 77 www
  • 06 文字数
  • 67 6f 6f 67 6c 65 google
  • 02 文字数
  • 63 6f co
  • 02 文字数
  • 6a 70 jp
  • 00 終端のnull
  • 00 01 どのレコードを問い合わせるか?について。1はAレコード
  • 00 01 どのクラスを問い合わせるのか?について。クラスって何やねん。1はインターネットを表すらしい...。
実際のDNSレスポンス例
0020   xx xx xx xx xx xx xx xx xx xx c4 db 81 80 00 01
0030   00 01 00 00 00 00 03 77 77 77 06 67 6f 6f 67 6c
0040   65 02 63 6f 02 6a 70 00 00 01 00 01 c0 0c 00 01
0050   00 01 00 00 00 0d 00 04 ac d9 a1 43
  • c4 db トランザクションIDです。リクエストと同じ。
  • 81 80 1000 0001 1000 0000です。前半8bitは上を見てください。
    • 1... .... サーバーがフルリゾルブ出来たかどうか。
    • ..0. .... DNSSECに成功していたら1となるフラグ。
    • .... 0000 応答コード。0はNo Error

次の8byteは4つのセクション(Question、Answer、Authority、Additional)の数を返すのですが、リクエストと比べてみるとAnswerの数が1増えていることが分かりますね。

  • 03 77 ~ 00 01 Questionセクションの中身。リクエストと同じものが返って来ています。
  • c0 0c 1100 0000 0000 1100です。
    • 最初の11..はポインタであることを示しているらしいです。
    • 残りの..00 0000 0000 1100は10進数だと12に相当するのですが、これはDNSレスポンスの先頭から12byte後を示していて、つまり03 77~でありwww.google.co.jpです。
  • 00 01 タイプがAレコードである事を示しています。
  • 00 01 インターネット・クラス(謎)
  • 00 00 00 0d TTL=13
  • 00 04 データの長さ
  • ac d9 a1 43 172.217.161.67

DNS over HTTPS (DoH)

DNSプロトコルは暗号化されてないので中身が丸見えで危険だし、そもそもDNSプロトコルって覚えるの面倒なんだけど。。。httpsでクエリ飛ばせないの?という状況を解決できるのがDoHです。

DoHリクエストの例
curl -H 'accept: application/dns-json' 'https://1.1.1.1/dns-query?name=www.google.co.jp&type=A'
DoHレスポンスの例
{"Status": 0,"TC": false,"RD": true, "RA": true, "AD": false,"CD": false,"Question":[{"name": "www.google.co.jp.", "type": 1}],"Answer":[{"name": "www.google.co.jp.", "type": 1, "TTL": 18, "data": "216.58.197.131"}]}

使いやすすぎワロタ

(追記)1.1.1.1はCloudflareの提供するPublic DNSサービスです。

WikipediaのPublic DNSサーバーの一覧ページなどでどのPublic DNSサーバーがDoHに対応しているかが分かります。

コネクションの確立

イメージ画像
スクリーンショット 2018-09-15 6.12.20.png

2. レンダリングエンジンが描画を行う

レスポンスボディで返ってきたhtmlドキュメントを元にブラウザに搭載されているレンダリングエンジンが画面を描画します。

Chromeの場合はBlinkというエンジンが搭載されています。
レイヤー図
(image from How Blink works by Kentaro Hara)

で、このBlinkで行われているレンダリングプロセスが下図です。
レンダーパイプライン
(image from How Blink works by Kentaro Hara)

htmlドキュメントを解釈してDOMを構築し、styleを当てはめてレイアウトしてから描画です。あとはLife of a Pixel 2018を見てください。マジで分かりやすいのでオススメです。これを見た後じゃあ自分で書き直そうという気にはなれませんね。

でも「どうしてもお前が書いた解説が読みたいんだ!!!」という人の為に書きます。正直これ以降の部分は質が微妙です。プロのあなたの助けを求めています。コメント・編集リクエストお待ちしています。

DOMとは

Document Object Modelの略です。chromiumのDOMのページに実装などの情報がまとまっています。

「いやDocument Object Modelって何やねん」となると思うのですが、端的にいうと木です。tree。

DOMはChromeで「⌘+⌥+i」を押すと出現する検証モードの「element」の項目に並んでいるアイツです。キーボードに⌘キーや⌥キーが無い場合はこちらへどうぞ。

(例)google.co.jpのDOM
DOCUMENT (always the root)
 └─HTML
   ├─HEAD
   │  └─...
   └─BODY
      ├─DIV
      │  └─...
      ├─SCRIPT
      └─SCRIPT

それぞれのnodeの実装
container node graph
(image from core/dom/README.md by きらきら☆はやとたん)
親はfirstChildとlastChildの情報しか持ってないそうです。連結リストってヤツですね。

レンダリングエンジンはHTML文章を解析し、上記のようなDOMツリーを構築します。

Style

レンダリングエンジンはCSSを解析してCSS Object Modelツリーを構築します。

元のCSS
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

⬇️CSSOM
CSSOM
(image from オブジェクト モデルの構築 by Ilya Grigorik)

DOMとCSSOMを組み合わせるとRender Treeが完成します。
render tree
(image from レンダリング ツリーの構築、レイアウト、ペイント by Ilya Grigorik)

ちなみにこのCSSOMはJavascriptにexpose(日本語訳求む)されているのでwindow.getComputedStyle() で要素のスタイルを取得する事ができます。

Layout

レンダリングツリーを元に、個々の要素がウィンドウ内のどこに配置されるかを決めていきます。

css box model
(image from CSS Box Model Module Level 3)

上の画像みたいなCSS Box Modelを使って、親から子へと再帰的に、つまり子は親のcontent内にレイアウトされていきます。CSSでは親要素との相対的な位置関係で記述されていますが、レイアウト処理の出力段階では画面上の絶対的な位置になります。

Life of a Pixel 2018 (1).png
(image from Life of a Pixel 2018)

Life of a Pixel 2018.png
(image from Life of a Pixel 2018)

中のコンテンツがスペースをオーバーしてもOKで、その時の選択肢は全て表示する「visible」、オーバーした部分を表示しない「hidden」、そしてスクロールできるようにする「scroll」の3つです。

  • SCROLLING IN BLINK | スクロールの仕組みについて。position: stickyって初めて知ったけどめっちゃ便利じゃないですか?もっと早く知りたかった……

後LayoutNGなる新しいレイアウトシステムが開発中らしい。

Paint

表示されるべき要素とそのスタイルが計算できたので、最後にそれぞれの要素を実際のピクセルに落とし込んでいきます。
Life of a Pixel 2018 (2).png
(image from Life of a Pixel 2018)

DOMツリーとは関係なく、描画リストの順番で描画されていきます。CSSのz-indexを設定する事で順番を明示的に指定できます。

Life of a Pixel 2018 (3).png
(image from Life of a Pixel 2018)

また、backgroundやfloatなどの要素によってどれが上に来るのかが変わるので、背後の要素が部分的に前に出て来ることもあるらしいです。

この描画工程が終わると画面にページが表示されます。

Compositor thread

ブラウザのレンダリング処理は全てmainスレッドという一つのスレッドで行われます。スレッドが一つしか無いというのは、javascriptなどで何か重たい処理を実行してしまいスレッドを埋めてしまった場合にスクロールなど基本的な操作すらおぼつかなくなってしまう危険があるということです。

そこで、スクロールやアニメーション、ズームなど「レイヤーの移動・スケールの変更」で対処できるものに関しては、レンダリングが終わった後に別スレッドへと移動させ、スムーズな操作を可能にしようというのが「Compositor Thread Architecture」です。
Life of a Pixel 2018 (4).png
(image from Life of a Pixel 2018)

プロの方々のコメントや編集リクエストをお待ちしています。よろしくお願いします。

この記事よりもLife of a Pixel 2018を読みましょう。ブラウザのレンダリングについて学ぶなら最高にオススメのスライドです。