ページの生成:ブラウザーはどのように動作するか

ユーザーは、読み込みが速く、スムーズにインタラクションできるコンテンツを、ウェブで体験することを求めています。したがって、開発者はこれらふたつの目標を達成するために努力しなければいけません。

どうやって性能そして体感性能を改善するか理解するためには、ブラウザーがどのように動作するかを理解することが役に立ちます。

概要

速いサイトはより良いユーザー体験をもたらします。ユーザーは読み込みが速く、スムーズにインタラクションできるコンテンツを体験することを求め、期待しています。

ウェブの性能における 2 つの主要な課題は、レイテンシーに関する諸問題と、多くの場合ブラウザーはシングルスレッドであるという事実に関する諸問題を理解することです。

レイテンシーは、速い読み込みを実現するために克服しなければいけない一番の脅威です。読み込みを速くしようとする開発者のゴールは、リクエストされた情報をできる限り速く送信すること、または少なくともとても速いように見せることです。ネットワークのレイテンシーは、バイト情報をコンピューターまで送信するのにかかる時間のことです。ウェブの性能は、ページの読み込みを出来る限り速くするよう私たちが何をするかにかかっています。

多くの場合、ブラウザーはシングルスレッドだと考えられます。スムーズなインタラクションを実現しようとする開発者のゴールは、滑らかなスクロール、タッチ操作への反応など、期待通りのサイトのインタラクションを実現することです。メインスレッドが全ての処理を時間内に完了し、その上でユーザーのインタラクションを常にハンドリングできるよう保証するために、描画時間が鍵となります。ブラウザーがシングルスレッドであることによる特性を理解し、メインスレッドの責務を最小限に抑え、可能かつ適切な場合に、描画のスムーズさとインタラクションへの即時の反応を実現することで、ウェブの性能が改善されます。

ナビゲーションはウェブページを読み込むための最初の一歩です。ユーザーが URL をアドレスバーに入力したり、リンクをクリックしたり、またはフォームを送信したり、ページをリクエストするたびごとにナビゲーションが発生します。

ウェブの性能における目標の 1 つは、ナビゲーションが完了するまでの時間を最小限にすることです。理想的な条件下において、一般的にこの時間が長すぎることはありませんが、レイテンシーと帯域幅が遅延を引き起こす邪魔者になる場合があります。

DNS ルックアップ

ウェブページへのナビゲーションの最初の一歩は、ページのアセットがどこにあるか見つけることです。https://example.com へアクセスする場合、その HTML のページは IP アドレスが 93.184.216.34 のサーバーに存在します。これまでに一度もそのサイトを訪れたことがなかった場合、DNS ルックアップが必要になります。

ブラウザーが DNS ルックアップをリクエストし、そのリクエストは最終的にネームサーバーによって処理され、ネームサーバーが IP アドレスを返します。この最初のリクエストの後、多くの場合その IP アドレスはしばらくの間キャッシュされ、ネームサーバーへ再接続する代わりにキャッシュから IP アドレスを取得することによって、後続するリクエストの速度を向上します。

DNS ルックアップは、一般的に、1 回のページ読み込みの中でホスト名ごとに 1 回だけ必要になります。しかし、DNS ルックアップは要求されたページが参照するユニークなホストネームそれぞれに対して実行が必要です。必要なフォントや画像、スクリプト、広告、メトリクスのそれぞれが異なるホスト名を持っている場合は、それぞれに対して DNS ルックアップが必要です。

Mobile requests go first to the cell tower, then to a central phone company computer before being sent to the internet

これはとくにモバイルネットワークにおいて性能上の問題になる可能性があります。ユーザーがモバイルネットワークを利用している場合、DNS ルックアップは、権威 DNS サーバーへ到達するために、電話機から基地局へ送信される必要があります。電話機と基地局、そしてネームサーバー間の距離によって重大な遅延が発生する場合があります。

TCP ハンドシェイク

IP アドレスが判明すると、ブラウザーは TCP 3 ウェイハンドシェイクを通じてサーバーとのコネクションを設定します。この仕組みは、通信を意図する 2 つの主体が—この場合はブラウザーとウェブサーバーが—、データを送信する前に、多くの場合 HTTPS を用いて、ネットワーク TCP ソケットコネクションのパラメーターを交換できるよう設計されています。

TCP の 3 ウェイハンドシェイクは、しばしば、"SYN-SYN-ACK"、より正確には SYN、SYN-ACK、ACK と呼ばれます。これは 2 つのコンピューターの間で TCP のセッションを開始するために、TCP によって 3 つのメッセージが送受信されることを表します。つまり、これはそれぞれのサーバーの間で 3 回以上のメッセージのやりとりが必要であり、そのためのリクエストが生成されなければいけないことを意味しています。

TLS ネゴシエーション

HTTPS によって確立される安全なコネクションでは、もう 1 つのハンドシェイクが必要です。このハンドシェイク、より正確に言うと TLS ネゴシエーションは、通信の暗号化に使用する暗号の種類を決定し、サーバーを認証し、実際のデータ送信が始まる前に安全な通信の準備を整えます。この処理は、コンテンツのリクエストを実際に送信する前に、さらに 3 回のラウンドトリップを必要とします。

The DNS lookup, the TCP handshake, and 5 steps of the TLS handshake including clienthello, serverhello and certificate, clientkey and finished for both server and client.

通信を安全にするためにページ読み込みの時間が追加されますが、ブラウザーとサーバーの間で送信されるデータが第三者に解読されないという安全な通信は、レイテンシーに見合う価値があるものです。

8 回のラウンドトリップを経て、ブラウザーはようやくリクエストを送ることができます。

レスポンス

ウェブサーバーへのコネクションが確立されると、ブラウザーはユーザーに代わって最初の HTTP GET リクエストを送信します。ウェブサイトであれば、多くの場合その対象は HTML ファイルです。リクエストを受け取ったサーバーは、適当なレスポンスヘッダーと HTML のコンテンツを返します。

<!doctype HTML>
<html>
 <head>
  <meta charset="UTF-8"/>
  <title>My simple page</title>
  <link rel="stylesheet" src="styles.css"/>
  <script src="myscript.js"></script>
</head>
<body>
  <h1 class="heading">My Page</h1>
  <p>A paragraph with a <a href="https://example.com/about">link</a></p>
  <div>
    <img src="myimage.jpg" alt="image description"/>
  </div>
  <script src="anotherscript.js"></script>
</body>
</html>

この最初のリクエストへのレスポンスは、受信データの最初のバイト (First Byte) を含んでいます。Time to First Byte (TTFB) は、ユーザーがリクエストを実行した時点から—たとえば、リンクをクリックした時点から— HTML の最初のパケットを受信するまでの時間を表します。コンテンツの最初のかたまり (Chunk) は一般的に 14kB のデータです。

上記のサンプルコードでは、リクエストされたコンテンツは明らかに 14kB より小さいですが、後述するように、リンクされたリソースは、ブラウザーのパース処理がそのリンクにたどり着くまでリクエストされることはありません。

TCP スロースタート / 14kB ルール

最初の応答パケットは 14kB になります。これは、TCP スロースタートの一部で、ネットワークコネクションの速度を制御するアルゴリズムの影響です。スロースタートは、ネットワークの最大の帯域幅が確定するまで徐々に送信するデータ量が増やします。

TCP スロースタートにおいて、サーバーは最初のパケットを受信した後、次のパケットのサイズを 28kB に倍増させます。さらに後続のパケットについて事前に決めた上限に達するか、ネットワークの輻輳を検知するまでサイズを増やします。

TCP slow start

最初のページ読み込みに関する 14kB ルールを聞いたことがあった場合、それは TCP スロースタートが理由です。そしてこれはウェブの性能の最適化が最初の 14kB にフォーカスする理由でもあります。TCP スロースタートは、ネットワークの輻輳を防ぎ、ネットワーク性能に対して適正なデータ転送速度を徐々に探り出します。

輻輳制御

サーバーが TCP パケットとしてデータを送信する一方で、ユーザー側のクライアントは確認応答、ACK を返してデータが配送されたことを確認します。コネクションには、ハードウェアやネットワークの条件によって許容量上の制限があります。サーバーが大きすぎるパケットを速すぎる速度で送信した場合、破棄されるでしょう。つまり、確認応答がない場合があるということです。サーバーはこれを missing ACK として処理します。輻輳制御アルゴリズムは、パケットを送信して確認応答を受け取るこのフローを使って送信レート (send rate) を決定します。

パース処理

データの最初のかたまりを受け取ると、ブラウザーは受信した情報のパース処理を始めることができます。パース処理は、ネットワークから受信したデータを DOMCSSOM に変換するステップです。DOM と CSSOM は、レンダラーがページを画面へ描画するために利用されます。

DOM はブラウザー向けのマークアップの内部的な表現です。DOM は外部に公開されており、JavaScript の様々な API を通じて操作できます。

ブラウザーは、リクエストしたページの HTML が最初の 14kB のパケットより大きかった場合でも、手元にあるデータに基づいてパース処理を開始し、サイトを描画しようとします。これは、ウェブの性能を最適化する時にに、ブラウザーがページの描画を始めるために必要なすべてのデータ、あるいは少なくともページのテンプレート (最初の描画に必要となる CSS と HTML) を最初の 14kB に含めることが重要となる理由です。何か 1 つでも画面に描画を行うには、その前に HTML と CSS、JavaScript がパースされなければいけません。

DOM ツリーの構築

クリティカルレンダリングパスの 5 つのステップを説明します。

最初のステップは HTML のマークアップを処理し、DOM ツリーを構築することです。HTML のパース処理は、トークン化とツリーの構築に分かれます。HTML のトークンは開始タグと終了タグ、属性の名前と値を含みます。ドキュメントが正しく構成されていればパース処理は単純で、速度も速くなります。パーサーはトークン化された入力情報をドキュメントツリーを構成するドキュメントに変換します。

DOM ツリーはドキュメントのコンテンツを表します。<html> 要素は最初のタグであり、ドキュメントツリーのルートノードとなります。ツリーには、異なるタグ同士の関係と階層構造が反映されます。他のタグの中にネストされたタグは子要素となります。DOM ノードの数が増えるほど、DOM ツリーの構築にかかる時間は長くなります。

The DOM tree for our sample code, showing all the nodes, including text nodes.

パーサーが画像のようなノンブロッキングリソースを発見した場合、ブラウザーはそのリソースをリクエストし、そのままパース処理を継続します。CSS ファイルに遭遇した場合もパース処理を継続できます、しかし  async または defer 属性がない <script> タグはレンダリングをブロックし、HTML のパース処理を停止します。ブラウザーのプリロードスキャナーがブロックの時間を減らしますが、それでも過度のスクリプトの使用は重大なボトルネックになり得ます。

プリロードスキャナー

ブラウザーが DOM ツリーを構築する間はそのプロセスがメインスレッドを占有します。その間にプリロードスキャナーが処理可能なコンテンツをパースし、CSS や JavaScript、ウェブフォントのような優先度の高いリソースのリクエストを行います。プリロードスキャナーのおかげで、リクエストするべき外部リソースへの参照をパーサーが見つけるのを待たなくて良くなります。バックグラウンドでリソースを取得するため、メインの HTML パーサーが該当のアセットにたどり着いた時には、すでにそれらのリソースが転送中あるいはダウンロード済みになっています。プリロードスキャナーによる最適化によりブロッキングを減らすることができます。

<link rel="stylesheet" src="styles.css"/>
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description"/>
<script src="anotherscript.js" async></script>

この例では、メインスレッドが HTML と CSS をパースしている間に、プリロードスキャナーがスクリプトと画像を探索し、それらのダウンロードを開始します。もし JavaScript の実行順序が重要でないなら、スクリプトがプロセスをブロックしないように async 属性または defer 属性を追加しましょう。

CSS の取得は HTML のパース処理あるいはダウンロードをブロックしません。しかし JavaScript の実行をブロックします。その理由は、しばしば JavaScript が CSS プロパティの要素への影響を問い合わせるために使われるからです。

CSSOM の構築

クリティカルレンダリングパスの 2 つめのステップは CSS を処理して CSSOM ツリーを構築することです。CSS のオブジェクトモデルは DOM によく似ています。DOM と CSSOM はどちらもツリー構造です。この 2 つは独立したデータ構造を持ちます。ブラウザーは、CSS のルールをブラウザーが理解できるスタイルのマップに変換します。ブラウザーは CSS のルールセットを読み取り、CSS セレクターにもとづいて、親、子、兄弟の関係から構成されるノードのツリーを生成します。

HTML の場合と同様に、ブラウザーは受信した CSS のルールをブラウザーが実行可能な形式に変換しなければいけません。そのために、ブラウザーは HTML をオブジェクトに変換する場合と同じプロセスを CSS に対しても実行します。

CSSOM ツリーはユーザーエージェントのスタイルシートから取得したスタイルを含みます。ブラウザーは、ノードに対して適用される最も一般的なルールからスタートして、より特定されたルールを再帰的に適用し、最終的なスタイルを計算します。つまり、プロパティの値をカスケードします。

CSSOM の構築はとても高速であるため、現在の開発者ツールはそれ自体をユニークな色で表示しません。その代わりに、開発者ツールの「スタイルの再計算」は、CSS を解析して CSSOM ツリーを構築し、再帰的にスタイルを計算するトータルの時間を表示します。ウェブの性能の最適化の観点から言うと、CSSOM を生成するトータルの時間は一般的に1回の DNS ルックアップにかかる時間よりも少ないため、それほどの苦労はないと言えます。

その他の処理

JavaScript のコンパイル

CSS がパースされ、CSSOM が生成される間、JavaScript ファイルを含む他のアセットが(プリロードスキャナーによって)ダウンロードされます。JavaScript は、インタープリターに処理され、コンパイル、パース処理を経て実行されます。スクリプトはパース処理によって抽象構文木に変換されます。いくつかのブラウザーエンジンは、抽象構文木をインタープリターへ引き渡し、メインスレッドで実行されるバイトコードを出力します。これが JavaScript のコンパイル処理に当たります。

アクセシビリティツリーの構築

ブラウザーはコンテンツを理解し翻訳する補助機器で使用されるアクセシビリティツリーも構築します。アクセシビリティオブジェクトモデル (AOM) は補助機器向けの DOM のようなものです。ブラウザーは、DOM が更新されるとアクセシビリティツリーも更新します。アクセシビリティツリーは補助機能それ自体からは変更できません。

AOM が構築されるまで、スクリーンリーダーでコンテンツにアクセスできません。

レンダリング

レンダリングのステップは、スタイル、レイアウト、ペイント、そしてコンポジットで構成されます。パースのステップで作成された CSSOM と DOM のツリーはレンダーツリーの形式へと組み合わされ、すべてのビジュアル要素のレイアウトを計算するために使用されてスクリーンに描画されます。いくつかのケースでは、CPU の代わりに GPU を使用してスクリーンの一部を描画し、メインスレッドを解放してパフォーマンスを改善するために、コンテンツ自身をレイヤーに昇格し、コンポジットを行います。

スタイル

クリティカルレンダリングパスの 3 番目のステップは DOM と CSSOM をレンダーツリーの形式へと組み合わせることです。計算されたスタイルのツリー、あるいはレンダーツリー、の構築は DOM ツリーのルートからスタートし、目に見える (Visible) ノードをトラバースします。

ユーザーエージェントのスタイルシートにある <head> のような表示されることないタグとその子要素、script { display: none; } のように display: none を指定されたすべてのノード、はレンダリングの結果に影響しないためレンダーツリーには含まれません。visibility: hidden が適用されたノードは、スペースを確保するためレンダーツリーに含まれます。上記のサンプルコード内の script ノードは、ユーザーエージェントのデフォルトを上書きするディレクティブが指定されていないためレンダーツリーに含まれません。

それぞれの目に見えるノードには、CSSOM のルールが適用されます。レンダーツリーはすべての目に見えるノードをコンテンツと計算されたスタイルを合わせて保持します。すべての関連するスタイルと DOM 上の目に見えるノードをマッチングし、CSS カスケードに基づいて、それぞれのノードに対応する計算されたスタイルを決定します。

レイアウト

クリティカルレンダリングパスの 4 番目のステップは各ノードの平面状の位置を計算するためにレイアウト処理を実行することです。レイアウトはレンダーツリーに含まれるすべてのノードの幅と高さ、位置を決める処理です。さらにページ上のそれぞれオブジェクトのサイズと位置を決定します。リフローは、続いて発生するドキュメント全体、あるいはページの一部分のサイズと位置を決める処理です。

レンダーツリーが構築されるとすぐにレイアウトが始まります。レンダーツリーは計算されたスタイルを踏まえてどのノードが表示されるか (非表示であっても) 特定しますが、寸法や位置は特定しません。各オブジェクトの正確なサイズと位置を決めるために、ブラウザーがレンダーツリーのルートからトラバースを行います。

ウェブページ上では、ほとんどすべての要素はボックスです。異なるデバイス、異なるデスクトップの設定は、ビューポートのサイズの数が無制限に存在することを示しています。このフェーズにおいて、ビューポートのサイズを考慮して、ブラウザーはすべての異なるボックスのスクリーン上の寸法を決定します。ビューポートのサイズを基本として、レイアウトは一般的にボディからスタートし、すべてのボディの子孫をそれぞれの要素のボックスモデルプロパティに合わせてレイアウトし、画像のように寸法がわからない代替要素のためのプレースホルダースペースを作成します。

ノードのサイズとポジションが決められる最初のタイミングをレイアウトと呼びます。続いて発生するノードのサイズと位置の再計算をリフロー呼びます。私たちの例では、画像が返される前に最初のレイアウトが発生すると考えられます。画像のサイズを宣言していなかったため、画像のサイズがわかるとすぐにリフローが発生します。

ペイント

クリティカルレンダリングパスの最後のステップは個別のノードをスクリーンにペイントすることです。最初に発生するペイントを first meaningful paint と呼びます。ペイントまたはラスタライゼーションのフェーズにおいて、ブラウザーはレイアウトフェーズで計算されたそれぞれのボックスをスクリーン上の実際のピクセルに変換します。ペイントは、テキスト、色、ボーダー、シャドウ、ボタンや画像のような置換要素を含む、要素のすべての視覚的な部分をスクリーンに描くことを含みます。ブラウザーはこれを超高速で実行する必要があります。

スムーズなスクロールとアニメーションを実現するために、スタイルの計算やリフロー、ペイントなどメインスレッドを占有するすべての処理は、16.67ms 未満で完了する必要があります。2048 x 1536 の解像度を持つ iPad は 3,145,000 を超えるピクセルを持っています。それら大量のピクセルは高速にペイントされなければいけません。2回目以降のペイントを最初のペイントより高速にするため、スクリーンへの描画は一般的に複数のレイヤーに分解されます。この場合にコンポジットが必要になります。

ペイントはペイントツリー内の要素をレイヤーに分解します。コンテンツを GPU (CPU 上のメインスレッドの代わりになる) 上のレイヤーに昇格させることで、ペイントと再ペイントのパフォーマンスを向上します。<video><canvas>など、レイヤーを生成する特定のプロパティと要素があります。opacity、3D transformwill-change、その他いくつかの CSS プロパティを持つ要素も同様です。これらのノードは、その子孫が上記の理由でそれ自身のレイヤーを必要とするのでなければ、子孫と一緒に自身のレイヤー上に描画されます。

レイヤーはパフォーマンスを改善しますが、メモリー管理の面ではコストのかかる処理です。そのため、ウェブのパフォーマンス最適化戦略の中で濫用するべきものではありません。

コンポジット

ドキュメントのセクションが異なるレイヤーに描画されて重なり合う場合、コンテンツをスクリーン上に正しい順番で描画するためにコンポジットが必要になります。

ページがアセットの読み込みを続ける間もリフローは発生します (後ほど出てくる図を見てください)。リフローは再ペイントと再コンポジットを引き起こします。画像のサイズを指定していた場合リフローは必要ありません。再ペイントが必要なレイヤーのみが再ペイントされ、必要があればコンポジットが行われます。しかし、私たちのサンプルでは画像のサイズを指定しませんでした。画像がサーバーから取得されたとき、レンダリングプロセスはレイアウトステップまで戻り、そこから再開します。

インタラクティビティ

メインスレッドがページのペイントを完了したら「これで終わり」と思うかもしれません。しかし、必ずしもそうとは言えません。読み込み処理が、遅延された onload イベントの発火により実行される JavaScript を含む場合、メインスレッドがビジー状態となりスクロールやタッチ、その他のインタラクションができない場合があります。

Time to Interactive (TTI) は、DNS ルックアップと SSL コネクション を始める最初のリクエストからページがインタラクティブになるまでどのくらい時間がかかったかを示す測定値です。インタラクティブであるとは、ページがユーザーのインタラクションに 50ms 以内に応答する First Contentful Paint の後の時点を言います。メインスレッドがパース処理、コンパイル、JavaScript の実行に占有されている場合、ユーザーのインタラクションにタイムリーに (50ms より早く) 応答することができません。

以下の例では、画像の読み込みは高速かもしれませんが、anotherscript.js ファイルが 2MB あり、しかもユーザーのネットワークは低速です。このケースでは、ユーザーはページのコンテンツをすぐに見ることができるかもしれませんが、スクリプトがダウンロードされるまでスクロールを実行できない可能性があります。これは良いユーザー体験とは言えません。この WebPageTest の例からわかるように、メインスレッドを占有することは避けなければいけません。

The main thread is occupied by the downloading, parsing and execution of a  javascript file - over a fast connection

この例では、DOM コンテンツの読み込みプロセスは 1.5 秒以上かかっており、メインスレッドがすべての時間を完全に占有され、クリックイベントや画面のタップに応答できなくなっています。

関連情報