レンダリング ツリーの構築、レイアウト、ペイント

CSSOM ツリーと DOM ツリーを組み合わせたものがレンダリング ツリーに含まれます。レンダリング ツリーは、各表示要素のレイアウトを計算するために使用され、画面にピクセルをレンダリングするペイント処理の入力になります。 レンダリングのパフォーマンスを最適化するには、これらの各ステップを最適化することが必須です。

オブジェクト モデルの構築について説明した前のセクションでは、HTML と CSS の入力をベースに DOM ツリーと CSSOM ツリーを構築しましたが、これらは異なる角度からドキュメントを表す独立したオブジェクトです。一方はコンテンツを記述し、もう一方はドキュメントに適用するスタイルルールを記述します。 この 2 つを結合してブラウザの画面上にピクセルをレンダリングするにはどうすればよいでしょうか。

TL;DR

  • DOM ツリーと CSSOM ツリーを組み合わせてレンダリング ツリーが形成されます。
  • レンダリング ツリーにはページのレンダリングに必要なノードのみが含まれています。
  • レイアウトでは各オブジェクトの正確な位置とサイズを計算します。
  • 最後のステップがペイントで、完成したレンダーツリーを取り込んで画面にピクセルをレンダリングします。

まずは、ブラウザで DOM と CSSOM を組み合わせて、ページ上の表示可能なすべての DOM コンテンツと、各ノードのすべての CSSOM スタイル情報を取り込んだ「レンダリング ツリー」を作成します。

DOM と CSSOM を組み合わせてレンダリング ツリーを作成

レンダリング ツリーを作成するため、ブラウザでは概ね次の処理を行います。

  1. DOM ツリーのルートから順に、表示される各ノードをトラバースします。

    • 一部のノード(スクリプトタグ、メタタグなど)は表示されることがなく、レンダリング出力に反映されないので省略されます。
    • CSS を介して非表示に設定されているノードも、同様にレンダリング ツリーから省略されます。たとえば、上記の例の span ノードには「display: none」プロパティを設定する明示的なルールが指定されているため、レンダリング ツリーに含まれません。
  2. 表示されるノードごとに、一致する適切な CSSOM ルールを見つけて適用します。

  3. コンテンツおよび計算済みスタイルを含めて、表示されるノードを出力します。

注: 少し本題からそれますが、visibility: hiddendisplay: none は異なります。「visibility: hidden」の場合、要素は非表示になりますが、依然としてレイアウト上のスペースを占めています(つまり、空のボックスとしてレンダリング)。これに対し、「display: none」の場合、要素はレンダリング ツリーから完全に削除されるので、要素は非表示となりレイアウトにも含まれません。

最終的に、画面上に表示可能なすべてのコンテンツとそのスタイル情報の両方を含むレンダーツリーが出力されます。レンダリング ツリーが正しく構築されたら、「レイアウト」段階に進むことができます。

ここまでで、表示されるノードとそのノードの計算済みのスタイルがわかりましたが、端末のビューポート内でのノードの正確な位置とサイズはまだわかりません。これを特定するのが「レイアウト」段階(別名「リフロー」)です。

ページ上の各オブジェクトの正確なサイズと位置を算定するため、ブラウザはレンダリング ツリーのルートから順にオブジェクトをトラバースします。実践的で簡単なサンプルで考察してみましょう。

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

サンプルを見る

上記のページの本体にはネストされた 2 つの div があります。1 つ目(親)の div はノードの表示サイズをビューポートの幅の 50% に設定し、この親に含まれている 2 つ目(子)の div はその幅を親の 50%、つまりビューポートの幅の 25% に設定します。

レイアウト情報の計算

レイアウト処理の出力は、ビューポート内の各要素の正確な位置とサイズを正確に取り込んだ「ボックスモデル」で、すべての相対的な測定値を画面上の絶対的なピクセルに変換する処理などが行われています。

表示されるノード、そのノードの計算済みのスタイル、形状がわかったので、この情報をもとに最終段階の処理を行うことができます。最終段階では、レンダリング ツリー内の各ノードを画面上の実際のピクセルに変換します。この手順を一般に「ペインティング」または「ラスタライジング」といいます。

ブラウザの処理量が非常に多いため、これには時間がかかります。しかし、Chrome DevTools を利用すると、上述の 3 段階すべての分析情報を得ることができます。元の「Hello World」の例で、レイアウト段階について見ていきましょう。

DevTools でのレイアウトの測定

  • 「Layout」イベントでは、Timeline でレンダリング ツリーの構築、位置、サイズを計算します。
  • レイアウトが完了すると、ブラウザは「Paint Setup」 イベントと「Paint」イベントを発行し、レンダリング ツリーが画面上のピクセルに変換されます。

レンダリング ツリーの構築、レイアウト、ペイントの実行に必要な時間は、ドキュメントのサイズ、適用されるスタイル、さらに実行端末によって異なります。ドキュメントが大きいほど、ブラウザで必要な処理も増え、スタイルが複雑なほど、ペインティングに要する時間も長くなります(たとえば、べた塗りは負荷が小さく、ドロップ シャドウの計算とレンダリングは負荷が大きくなります)。

すべて完了すると、ページがビューポートに表示されます。

レンダリングされた Hello World ページ

以下に、ブラウザで行われた手順を簡単にまとめます。

  1. HTML マークアップを処理して DOM ツリーを構築する。
  2. CSS マークアップを処理して CSSOM ツリーを構築する。
  3. DOM と CSSOM を組み合わせてレンダリング ツリーを構成する。
  4. レンダリング ツリーでレイアウトを実行して各ノードの形状を計算する。
  5. 各ノードを画面にペイントする。

このデモページはシンプルに見えるかもしれませんが、かなりの処理が必要です。DOM または CSSOM が変更されると、このプロセスを繰り返して、画面上で再レンダリングが必要なピクセルを判別する必要があります。

クリティカル レンダリング パスを最適化することで、上記の手順 1~5 の合計所要時間を最小限に抑えることができます。 その結果、できるだけ短時間で画面にコンテンツをレンダリングできるようになり、初回レンダリング後の画面の更新間隔も短くなります。つまり、インタラクティブなコンテンツの場合に、高いリフレッシュ レートを実現できます。