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

Mariko Kosaka

レンダラ プロセスの内部動作

これは、ブラウザの仕組みを解説する全 4 回のブログシリーズの第 3 部です。前回は、マルチプロセス アーキテクチャナビゲーション フローについて説明しました。今回の投稿ではレンダラプロセス内での 処理について説明します

レンダラ プロセスはウェブ パフォーマンスのさまざまな側面に影響します。レンダラ プロセス内ではさまざまな処理が行われるため、この投稿では一般的な概要のみとしています。さらに詳しい情報については、ウェブの基礎のパフォーマンスのセクションをご覧ください。

レンダラ プロセスによるウェブ コンテンツの処理

レンダラ プロセスは、タブ内で発生するすべての処理を担います。 レンダラ プロセスでは、ユーザーに送信するコードのほとんどをメインスレッドが処理します。ウェブワーカーまたは Service Worker を使用している場合、JavaScript の一部がワーカー スレッドによって処理されることがあります。コンポジタ スレッドとラスター スレッドもレンダラ プロセス内で実行され、ページを効率的かつスムーズにレンダリングします。

レンダラ プロセスの主な役割は、HTML、CSS、JavaScript を、ユーザーが操作できるウェブページに変換することです。

レンダラのプロセス
図 1: メインスレッド、ワーカー スレッド、コンポジタ スレッド、ラスター スレッドを内部に含むレンダラ プロセス

解析

DOM の構築

レンダラ プロセスがナビゲーションの commit メッセージを受け取り、HTML データの受信を開始すると、メインスレッドはテキスト文字列(HTML)の解析を開始し、ドキュメントオブジェクトDOM)に変換します。

DOM はブラウザのページの内部表現であり、ウェブ デベロッパーが JavaScript を介して操作できるデータ構造と API でもあります。

HTML ドキュメントの DOM への解析は HTML 標準で定義されています。ブラウザに HTML を入力してもエラーはスローされません。たとえば、</p> 終了タグがない場合は、有効な HTML です。Hi! <b>I'm <i>Chrome</b>!</i>(i タグの前に b タグが閉じられている)などの誤ったマークアップは、Hi! <b>I'm <i>Chrome</i></b><i>!</i> を記述したかのように扱われます。これは、HTML 仕様が、このようなエラーを適切に処理するように設計されているためです。これらの処理の詳細については、HTML 仕様のパーサーにおけるエラー処理と奇妙なケースの概要をご覧ください。

サブリソースを読み込んでいます

ウェブサイトでは通常、画像、CSS、JavaScript などの外部リソースを使用します。これらのファイルは、ネットワークまたはキャッシュから読み込む必要があります。メインスレッドが DOM 構築の解析中に見つかったときに 1 つずつリクエストすることもできますが、高速化のため「プリロード スキャナ」は同時に実行します。HTML ドキュメントに <img><link> などがある場合、プリロード スキャナは HTML パーサーによって生成されたトークンを照合し、ブラウザ プロセスのネットワーク スレッドにリクエストを送信します。

DOM
図 2: HTML を解析して DOM ツリーを構築するメインスレッド

JavaScript は、この関数によって

HTML パーサーは、<script> タグを検出すると HTML ドキュメントの解析を一時停止し、JavaScript コードの読み込み、解析、実行を行う必要があります。これは、JavaScript が DOM 構造全体を変更する document.write() などを使用してドキュメントの形状を変更できるためです(HTML 仕様の解析モデルの概要では適切な図があります)。このため、HTML パーサーは JavaScript の実行を待ってから、HTML ドキュメントの解析を再開する必要があります。JavaScript を実行するとどうなるかに興味がある方は、V8 チームが講演やブログ記事を投稿しています

リソースをブラウザに読み込む方法のヒント

ウェブ デベロッパーがブラウザにヒントを送信してリソースを適切に読み込むには、さまざまな方法があります。 JavaScript で document.write() を使用していない場合は、async 属性または defer 属性を <script> タグに追加できます。ブラウザは JavaScript コードを非同期で読み込んで実行し、解析をブロックしません。必要に応じて、JavaScript モジュールを使用することもできます。<link rel="preload"> は、リソースが現在のナビゲーションに不可欠であり、できるだけ早くダウンロードする必要があることをブラウザに知らせる手段です。詳しくは、リソースの優先順位付け – ブラウザを利用するメリットをご覧ください。

スタイルの計算

CSS でページ要素のスタイルを設定できるため、DOM があるだけでは、ページがどのように表示されるかを知るには不十分です。メインスレッドは CSS を解析し、各 DOM ノードの計算済みスタイルを決定します。これは、CSS セレクタに基づいて各要素に適用されるスタイルの種類に関する情報です。この情報は、DevTools の computed セクションで確認できます。

計算済みスタイル
図 3: CSS を解析して計算済みスタイルを追加するメインスレッド

CSS を指定しない場合でも、各 DOM ノードは計算済みスタイルを持ちます。<h1> タグは <h2> タグよりも大きく表示され、要素ごとにマージンが定義されます。これは、ブラウザにデフォルトのスタイルシートがあるためです。Chrome のデフォルトの CSS については、こちらでソースコードを確認できます

Layout

これで、レンダラ プロセスはドキュメントの構造と各ノードのスタイルを認識しましたが、これだけではページをレンダリングするには不十分です。スマートフォンで友人に絵画について説明しようとしているとします。「大きな赤い円と小さな青い四角がある」というだけでは、友人が絵画がどのようなものになるかを正確に知るには不十分です。

人間 FAX 機のゲーム
図 4: 絵の前に立つ人物、相手につながる電話回線

レイアウトとは、要素のジオメトリを見つけるプロセスです。メインスレッドは、DOM と計算済みスタイルを処理して、x y 座標や境界ボックスのサイズなどの情報を含むレイアウト ツリーを作成します。レイアウト ツリーは DOM ツリーの構造に似ている場合がありますが、ページに表示されている内容に関連する情報のみが含まれています。display: none が適用された場合、その要素はレイアウト ツリーには含まれません(ただし、visibility: hidden の要素はレイアウト ツリーに含まれます)。同様に、p::before{content:"Hi!"} のようなコンテンツを含む疑似要素が適用されると、DOM になくても、レイアウト ツリーに含まれます。

layout
図 5: 計算済みスタイルで DOM ツリーに移動し、レイアウト ツリーを生成しているメインスレッド
図 6: 改行変更によって移動する段落のボックス レイアウト

ページのレイアウトを決定するのは、簡単な作業ではありません。上から下へのブロックフローのような単純なページ レイアウトであっても、フォントのサイズと改行する場所を考慮する必要があります。これは、段落のサイズと形状に影響します。これにより、次の段落の配置場所に影響します。

CSS では、要素を片側にフローティング表示したり、オーバーフロー アイテムをマスクしたり、書き込み方向を変更したりできます。ご想像のとおり、このレイアウト ステージには複雑なタスクがあります。Chrome では、エンジニア チーム全体がレイアウトに取り組んでいます。その活動の詳細をご覧になりたい場合は、BlinkOn Conference の講演がいくつか録画されており、とても興味深いものです。

Paint

描画ゲーム
図 7: キャンバスの前で絵筆を持ち、円を描くべきか四角形を最初に描くべきかわからない人物

DOM、スタイル、レイアウトを用意するだけでは、ページをレンダリングできません。絵画を再現するとします要素のサイズ、形状、位置は把握していますが、描画する順序を判断する必要があります。

たとえば、特定の要素に z-index が設定されている場合、HTML に記述された要素の順序でペイントすると、レンダリングが正しく行われません。

Z-Index のエラー
図 8: HTML マークアップの順序でページ要素が表示され、Z-Index が考慮されていなかったため、レンダリングされた画像が誤って表示される

このペイント ステップでは、メインスレッドがレイアウト ツリーに沿ってペイント レコードを作成します。ペイント レコードは、「最初に背景、次にテキスト、次に長方形」のようなペイント プロセスのメモです。JavaScript を使用して <canvas> 要素に描画したことがある場合、このプロセスは見覚えがあるでしょう。

ペイント レコード
図 9: レイアウト ツリーをたどってペイント レコードを生成するメインスレッド

レンダリング パイプラインの更新には費用がかかる

図 10: 生成順序の DOM+Style、Layout、Paint ツリー

レンダリング パイプラインで把握しておくべき最も重要なことは、各ステップで前のオペレーションの結果を使用して新しいデータが作成されることです。たとえば、レイアウト ツリーになんらかの変更を加えた場合、ドキュメントの影響を受ける部分のペイント順序を再生成する必要があります。

要素をアニメーション化する場合、ブラウザはフレームごとにこれらのオペレーションを実行する必要があります。Google のディスプレイのほとんどは、画面を 1 秒に 60 回(60 fps)更新します。フレームごとに画面上で物を動かすと、人間の目にはアニメーションが滑らかに表示されます。ただし、アニメーション間のフレームが欠落している場合、ページは「ジャンク」のように見えます。

フレーム欠落による Jage ジャンク
図 11: タイムライン上のアニメーション フレーム

レンダリング オペレーションが画面の更新に対応していても、これらの計算はメインスレッドで実行されているため、アプリケーションが JavaScript を実行している場合はブロックされる可能性があります。

JavaScript による Jage ジャンク
図 12: タイムライン上のアニメーション フレーム(1 つのフレームが JavaScript によってブロックされている)

JavaScript オペレーションを小さなチャンクに分割し、requestAnimationFrame() を使用してフレームごとに実行するようにスケジュールできます。このトピックについて詳しくは、JavaScript 実行の最適化をご覧ください。メインスレッドがブロックされないように、ウェブ ワーカーで JavaScript を実行することもできます。

リクエストのアニメーション フレーム
図 13: アニメーション フレームのあるタイムライン上で実行される JavaScript の小さなチャンク

合成

ページをどのように描画しますか。

図 14: 単純なラスター処理のアニメーション

ブラウザはドキュメントの構造、各要素のスタイル、ページのジオメトリ、ペイント順序を認識しました。では、ページをどのように描画するのでしょうか。この情報を画面上のピクセルに変換することを ラスタライズと呼びます

おそらく、ビューポート内でパーツをラスターで処理する方法が単純ではないでしょう。ユーザーがページをスクロールした場合は、ラスター化されたフレームを移動し、欠けている部分をラスター化して埋めます。最初のリリース時の Chrome のラスタライズはこのように処理されていました。ただし、最新のブラウザでは、合成と呼ばれるより高度なプロセスが実行されます。

合成とは

図 15: 合成プロセスのアニメーション

合成とは、ページの一部をレイヤに分割して個別にラスタライズし、コンポジタ スレッドと呼ばれる別のスレッドにページとして合成する技術です。スクロールが発生した場合、レイヤはすでにラスタライズされているため、必要な作業は新しいフレームを合成することだけです。アニメーションは、レイヤを移動して新しいフレームを合成することで、同じ方法で実現できます。

ウェブサイトが複数のレイヤに分割される仕組みは、DevTools の [Layers] パネルで確認できます。

レイヤへの分割

どの要素をどのレイヤに含める必要があるかを判断するために、メインスレッドはレイアウト ツリーに沿ってレイヤツリーを作成します(この部分は DevTools のパフォーマンス パネルで「Update Layer Tree」と呼ばれます)。独立したレイヤにする必要があるページの一部(スライドインのサイドメニューなど)にレイヤが表示されない場合は、CSS で will-change 属性を使用してブラウザにヒントを提供できます。

レイヤツリー
図 16: レイヤツリーを生成するレイアウト ツリーを通過するメインスレッド

すべての要素にレイヤを追加したくても、過剰な数のレイヤにわたって合成を行うと、フレームごとにページの小さな部分をラスタライズするよりもオペレーションが遅くなる可能性があるため、アプリのレンダリング パフォーマンスを測定することが重要です。このトピックについて詳しくは、コンポジタ専用プロパティの使用とレイヤ数の管理をご覧ください。

メインスレッドからのラスターと合成

レイヤツリーが作成され、ペイント順序が決定されると、メインスレッドはその情報をコンポジタ スレッドに commit します。次に、コンポジタ スレッドが各レイヤをラスタライズします。レイヤはページ全体と同じ大きさになることがあるため、コンポジタ スレッドはタイルに分割し、各タイルをラスター スレッドに送信します。ラスター スレッドは各タイルをラスタライズして GPU メモリに保存します。

ラスター
図 17: タイルのビットマップを作成して GPU に送信するラスター スレッド

コンポジタ スレッドは、異なるラスター スレッドに優先順位を付け、ビューポート内(または近くにある)を最初にラスター化できるようにします。また、ズームイン アクションなどを処理するために、解像度に応じた複数のタイリングも用意されています。

タイルがラスター化されると、コンポジタ スレッドは描画クワッドと呼ばれるタイル情報を収集し、コンポジタ フレームを作成します。

大腿四頭筋を描く メモリ内のタイルの位置や、ページ合成を考慮してタイルを描画するページ内の場所などの情報が含まれます。
コンポジタ フレーム ページのフレームを表すクワッドの集合。

コンポジタ フレームが IPC を介してブラウザ プロセスに送信されます。この時点で、ブラウザ UI の変更のために UI スレッドから別のコンポジタ フレームを追加したり、拡張機能用の他のレンダラ プロセスから別のコンポジタ フレームを追加したりできます。これらのコンポジタ フレームは、画面に表示するために GPU に送信されます。スクロール イベントが発生すると、コンポジタ スレッドは GPU に送信する別のコンポジタ フレームを作成します。

合成
図 18: 合成フレームを作成するコンポジタ スレッド。フレームはブラウザ プロセス、そして GPU に送信されます

コンポジットの利点は、メインスレッドを介さずにコンポジットできることです。コンポジタ スレッドは、スタイルの計算や JavaScript の実行を待つ必要はありません。そのため、スムーズなパフォーマンスを実現するには、アニメーションのみの合成が最適であると考えられます。レイアウトまたはペイントを再度計算する必要がある場合は、メインスレッドが関与する必要があります。

まとめ

この投稿では、レンダリング パイプラインの解析から合成までを見てきました。ここまでで、ウェブサイトのパフォーマンス最適化について理解を深めていただけたなら幸いです。

このシリーズの次の投稿と最後の投稿では、コンポジタ スレッドを詳しく見て、mouse moveclick などのユーザー入力を受け取ったときにどうなるかを見ていきます。

投稿はいかがでしたか?今後の投稿についてご不明な点やご提案がございましたら、以下のコメント欄または Twitter の @kosamari からお知らせください。

次のステップ: 入力はコンポジタに送られる