最新のウェブブラウザの詳細(パート 2)

Mariko Kosaka

ナビゲーションでの流れ

これは、Chrome の内部構造を紹介する 4 部構成のブログシリーズのパート 2 です。 前回の投稿では、さまざまなプロセスやスレッドがブラウザの各部分を処理する方法について説明しました。この投稿では ウェブサイトを表示するために 各プロセスとスレッドの通信の仕組みを深く掘り下げます

ウェブブラウザの簡単なユースケースを見てみましょう。ブラウザに URL を入力すると、ブラウザがインターネットからデータを取得してページを表示します。この投稿では ユーザーがサイトをリクエストし ブラウザがページをレンダリングする準備をする 部分に焦点を当てます

ブラウザ プロセスから始まり

ブラウザのプロセス
図 1: ブラウザ UI(上部)、内部に UI、ネットワーク、ストレージ スレッドがあるブラウザ プロセスの図

パート 1: CPU、GPU、メモリ、マルチプロセス アーキテクチャで説明したように、タブ以外のものはすべてブラウザ プロセスによって処理されます。ブラウザ プロセスには、ブラウザのボタンや入力フィールドを描画する UI スレッド、インターネットからデータを受け取るネットワーク スタックを扱うネットワーク スレッド、ファイルへのアクセスを制御するストレージ スレッドなどのスレッドがあります。アドレスバーに URL を入力すると、入力はブラウザ プロセスの UI スレッドによって処理されます。

シンプルなナビゲーション

ステップ 1: 入力を処理する

ユーザーがアドレスバーへの入力を開始すると、UI スレッドで最初に「これは検索クエリと URL のどちらですか?」と尋ねられます。Chrome では、アドレスバーは検索入力フィールドでもあるため、検索エンジンとリクエストしたサイトのどちらにリダイレクトするかを、UI スレッドが解析して決定する必要があります。

ユーザー入力の処理
図 1: 入力が検索クエリと URL のどちらであるかを尋ねる UI スレッド

ステップ 2: ナビを開始する

ユーザーが Enter キーを押すと、UI スレッドがサイト コンテンツを取得するためのネットワーク呼び出しを開始します。読み込みスピナーがタブの隅に表示され、ネットワーク スレッドが DNS ルックアップやリクエストの TLS 接続の確立などの適切なプロトコルを通過します。

ナビ開始
図 2: UI スレッドがネットワーク スレッドと通信して 🚫?.com に移動する

この時点で、ネットワーク スレッドは HTTP 301 などのサーバー リダイレクト ヘッダーを受け取ることがあります。その場合、ネットワーク スレッドは、サーバーがリダイレクトをリクエストしていることを UI スレッドと通信します。その後、別の URL リクエストが開始されます。

ステップ 3: レスポンスを読み取る

HTTP レスポンス
図 3: Content-Type と、実際のデータであるペイロードを含むレスポンス ヘッダー

レスポンスの本文(ペイロード)の受信が開始されると、ネットワーク スレッドは必要に応じてストリームの最初の数バイトを確認します。レスポンスの Content-Type ヘッダーにはデータの種類が示されていますが、これは欠落しているか間違っている可能性があるため、MIME タイプ スニッフィングを行います。ソースコードでコメントされているように、これは「厄介なビジネス」です。 このコメントを読み、各ブラウザでのコンテンツ タイプとペイロードのペアの処理方法を確認できます。

レスポンスが HTML ファイルの場合、次のステップとしてレンダラ プロセスにデータを渡します。一方 ZIP ファイルまたはその他のファイルの場合はダウンロード リクエストであるため、ダウンロード マネージャーにデータを渡す必要があります。

MIME タイプ スニッフィング
図 4: レスポンス データが安全なサイトの HTML であるかどうかを尋ねるネットワーク スレッド

このタイミングでSafeBrowsingのチェックも行われます。 ドメインとレスポンス データが既知の悪意のあるサイトと一致する可能性がある場合、ネットワーク スレッドは警告ページを表示するようにアラートを表示します。さらに、機密性の高いクロスサイト データがレンダラ プロセスに送られないことを確認するために、Cross Origin Read Blocking(CORBチェックが行われます。

ステップ 4: レンダラ プロセスを見つける

すべてのチェックが完了し、リクエストされたサイトにブラウザからアクセスできることがネットワーク スレッドによって確認されると、ネットワーク スレッドは UI スレッドにデータの準備ができたことを伝えます。UI スレッドは、ウェブページのレンダリングを続けるレンダラ プロセスを見つけます。

レンダラ プロセスの検索
図 5: レンダラ プロセスを見つけるよう UI スレッドに指示するネットワーク スレッド

ネットワーク リクエストはレスポンスを返すまでに数百ミリ秒かかる場合があるため、このプロセスを高速化するための最適化が適用されます。ステップ 2 で UI スレッドが URL リクエストをネットワーク スレッドに送信しているとき、UI スレッドは移動先のサイトをすでに認識しています。UI スレッドは、ネットワーク リクエストと並行して、事前にレンダラ プロセスを検出または開始しようとします。これにより、すべてが想定どおりになっても、ネットワーク スレッドがデータを受信したときに、レンダラ プロセスはすでにスタンバイ状態になっています。ナビゲーションがクロスサイト リダイレクトをリダイレクトする場合は、このスタンバイ プロセスが使用されない可能性があり、その場合は別のプロセスが必要になることがあります。

ステップ 5: commit のナビゲーション

データとレンダラ プロセスの準備ができたら、IPC がブラウザ プロセスからレンダラ プロセスに送信され、ナビゲーションが commit されます。また、レンダラ プロセスが HTML データを受信し続けることができるように、データ ストリームも渡します。ブラウザ プロセスが、レンダラ プロセスで commit が発生したことを確認すると、ナビゲーションが完了し、ドキュメントの読み込みフェーズが開始します。

この時点でアドレスバーが更新され、セキュリティ インジケーターとサイト設定 UI に新しいページのサイト情報が反映されます。タブのセッション履歴が更新されるため、[戻る] ボタンと [進む] ボタンで移動先のサイトを順に表示できます。タブやウィンドウを閉じる際にタブやセッションの復元がしやすくなるように、セッション履歴はディスクに保存されます。

ナビゲーションを commit
図 6: ブラウザとレンダラ プロセス間の IPC がページのレンダリングをリクエストする

追加ステップ: 初期読み込みの完了

ナビゲーションが commit されると、レンダラ プロセスがリソースの読み込みを進め、ページをレンダリングします。この段階で何が行われるかについては、次回の投稿で詳しく説明します。レンダラ プロセスがレンダリングを「終了」すると、IPC をブラウザ プロセスに返します(ページ内のすべてのフレームですべての onload イベントが発生し、実行を完了した後)。この時点で、UI スレッドはタブの読み込みスピナーを停止します。

「完了」と呼ぶのは、この時点の後もクライアント側の JavaScript で追加のリソースの読み込みや新しいビューのレンダリングを行えるためです。

ページ終了時読み込み
図 7: レンダラからブラウザ プロセスへの IPC による、ページの「読み込み」完了

これでシンプルなナビゲーションは完了です。しかし、ユーザーがアドレスバーに別の URL を再度入力するとどうなるでしょうか。このように、ブラウザのプロセスは同じ手順で別のサイトに移動します。ただし、これを行う前に、現在レンダリングされているサイトに beforeunload イベントに関心があるかを確認する必要があります。

beforeunload では、ユーザーがページから移動しようとしたときやタブを閉じようとしたときに、「このサイトから離れますか?」というアラートを作成できます。JavaScript コードを含め、タブ内のものはすべてレンダラ プロセスによって処理されるため、新しいナビゲーション リクエストが届いたとき、ブラウザ プロセスは現在のレンダラ プロセスを確認する必要があります。

beforeunload イベント ハンドラ
図 8: ブラウザ プロセスからレンダラ プロセスへの IPC から別のサイトに移動しようとしていることを示す

ナビゲーションがレンダラ プロセスから開始された場合(ユーザーがリンクをクリックした場合や、クライアント側の JavaScript が window.location = "https://newsite.com" を実行した場合など)、レンダラ プロセスは最初に beforeunload ハンドラを確認します。その後、ブラウザ プロセスで開始されたナビゲーションと同じプロセスを実行します。唯一の違いは、ナビゲーション リクエストがレンダラ プロセスからブラウザ プロセスに開始される点です。

現在レンダリングされているサイトとは異なるサイトに新しいナビゲーションが行われると、新しいナビゲーションを処理するために別のレンダリング プロセスが呼び出され、unload などのイベントを処理するために現在のレンダリング プロセスが保持されます。詳しくは、ページのライフサイクルの状態の概要と、Page Lifecycle API でイベントを処理する方法をご覧ください。

新しいナビゲーションとアンロード
図 9: ブラウザ プロセスから新しいレンダラ プロセスへの、ページのレンダリングを指示し、古いレンダラ プロセスへのアンロードを指示する 2 つの IPC

Service Worker の場合

このナビゲーション プロセスに対する最近の変更の一つは、Service Worker の導入です。Service Worker は、アプリケーション コードにネットワーク プロキシを記述する手段です。これにより、ウェブ デベロッパーは、ローカルにキャッシュする対象や、ネットワークから新しいデータを取得するタイミングをより細かく制御できます。キャッシュからページを読み込むように Service Worker が設定されている場合、ネットワークからデータをリクエストする必要はありません。

注意すべき重要な点は、Service Worker はレンダラ プロセスで実行される JavaScript コードであるということです。しかし、ナビゲーション リクエストが届いたとき、ブラウザ プロセスはサイトに Service Worker が存在することをどのようにして認識するのでしょうか。

Service Worker のスコープの検索
図 10: Service Worker のスコープを検索するブラウザ プロセスのネットワーク スレッド

Service Worker が登録されると、Service Worker のスコープが参照として保持されます(スコープの詳細については、こちらの Service Worker のライフサイクルの記事をご覧ください)。ナビゲーションが発生すると、ネットワーク スレッドはドメインを登録済みの Service Worker スコープと照合します。Service Worker がその URL に登録されている場合、UI スレッドは Service Worker コードを実行するレンダラ プロセスを見つけます。Service Worker はキャッシュからデータを読み込むことで、ネットワークにデータをリクエストする必要がなくなります。また、ネットワークから新しいリソースをリクエストすることもできます。

Service Worker のナビゲーション
図 11: Service Worker を処理するレンダラ プロセスを起動するブラウザ プロセスの UI スレッド。レンダラ プロセスのワーカー スレッドがネットワークからのデータをリクエストする

ブラウザ プロセスとレンダラ プロセス間のこのラウンドトリップにより、最終的に Service Worker がネットワークからデータをリクエストすると、遅延が発生する可能性があります。 ナビゲーション プリロードは、Service Worker の起動と並行してリソースを読み込むことで、このプロセスを高速化するメカニズムです。これらのリクエストをヘッダーでマークして、サーバーはこれらのリクエストに対して別のコンテンツを送信するかどうかを決定できます。たとえば、ドキュメント全体ではなく更新されたデータのみを送信できます。

ナビゲーションのプリロード
図 12: ネットワーク リクエストを並行して開始しながら Service Worker を処理するレンダラ プロセスを起動するブラウザ プロセスの UI スレッド

まとめ

この投稿では、ナビゲーション中の処理と、レスポンス ヘッダーやクライアントサイド JavaScript などのウェブ アプリケーション コードがブラウザとどのようにやり取りするかについて説明しました。ブラウザからデータを取得するためにブラウザがたどるステップを知ることで、ナビゲーション プリロードなどの API が開発された理由を理解しやすくなります。次回の投稿では、ブラウザが HTML/CSS/JavaScript を評価してページをレンダリングする仕組みについて詳しく説明します。

この投稿はお楽しみいただけましたか?今後の投稿についてご質問やご提案がありましたら、以下のコメント セクションまたは Twitter の @kosamari からお寄せください。

次のステップ: レンダラ プロセスの内部動作