Chrome のアクセラレーテッド レンダリング

レイヤモデル

Tom Wiltzius
Tom Wiltzius

はじめに

ほとんどのウェブ デベロッパーにとって、ウェブページの基本モデルは DOM です。レンダリングとは、多くの場合、ページの表示を画面上の画像に変換する不明瞭なプロセスです。最近のブラウザでは、近年、レンダリングの仕組みが変化し、グラフィック カードが活用されています。これは多くの場合、あいまいに「ハードウェア アクセラレーション」と呼ばれています。通常のウェブページ(Canvas2D や WebGL ではない)について話すとき、この用語は実際には何を意味するでしょうか?この記事では、Chrome におけるウェブ コンテンツのハードウェア アクセラレーション レンダリングを支える基本モデルについて説明します。

脂肪に関する大きな注意事項

ここでは WebKit について説明しますが、特に WebKit の Chromium ポートについて説明します。この記事では、ウェブ プラットフォームの機能ではなく、Chrome の実装の詳細について説明します。ウェブ プラットフォームや標準では、このレベルの実装の詳細が体系化されていないため、この記事の内容が他のブラウザにも適用される保証はありませんが、内部構造の知識は高度なデバッグやパフォーマンス調整に役立ちます。

また、この記事では、急速に変化する Chrome のレンダリング アーキテクチャの中核部分を取り上げています。この記事では、ほとんど変更されない情報のみを取り上げていますが、6 か月後にすべて変更されるとは限りません。

Chrome には、ハードウェア アクセラレーション パスと古いソフトウェア パスという 2 種類のレンダリング パスが以前からあることを理解しておくことが重要です。この記事の執筆時点では、すべてのページが Windows、ChromeOS、Chrome for Android のハードウェア アクセラレーションに対応しています。Mac と Linux では、一部のコンテンツで合成が必要なページのみが高速化されます(合成が必要なページについては後述します)。ただし、まもなくすべてのページも高速化されます。

最後に、レンダリング エンジンの仕組みを掘り下げ、パフォーマンスに大きな影響を与える機能について見ていきます。自分のサイトのパフォーマンスを向上させようとするときは、レイヤモデルを理解しておくと役立ちますが、自力で考えることも簡単です。レイヤは有用な構成要素ですが、多数のレイヤを作成すると、グラフィック スタック全体にオーバーヘッドが生じる可能性があります。入念な準備が必要です。

DOM から画面へ

レイヤの概要

ページの読み込みと解析が完了すると、そのページは多くのウェブ デベロッパーになじみのある DOM 構造としてブラウザに表示されます。しかし、ページをレンダリングする際、ブラウザには、開発者に直接公開されない一連の中間表現があります。これらの構造の中で最も重要なのがレイヤです。

Chrome には、実際には DOM のサブツリーを管理する RenderLayers と、RenderLayer のサブツリーを管理する GraphicsLayers という、さまざまな種類のレイヤがあります。ここで最も興味を引くのは後者です。GraphicsLayers はテクスチャとして GPU にアップロードされるからです。ここからは、「Layer」とは GraphicsLayer を意味します。

GPU の用語を簡単に説明: テクスチャとはこれは、メインメモリ(RAM)からビデオメモリ(GPU 上の VRAM)に移動されるビットマップ画像だと考えてください。GPU に配置したら、それをメッシュ ジオメトリにマッピングできます。ビデオゲームや CAD プログラムでは、この手法を使用してスケルトン 3D モデルに「スキン」を施します。Chrome はテクスチャを使用してウェブページ コンテンツのチャンクを GPU に取り込みます。テクスチャは、非常にシンプルな長方形のメッシュに応用することで、低コストでさまざまな位置や変形にマッピングできます。これが 3D CSS の仕組みであり、高速スクロールにも適していますが、この両方については後ほど詳しく説明します。

レイヤのコンセプトを説明するために、いくつかの例を見てみましょう。

Chrome でレイヤを調べる際に非常に便利なツールは、デベロッパー ツールの設定にある「レンダリング」の見出しの下にある「合成レイヤの枠線を表示」フラグ(小さな歯車アイコン)です。画面上のレイヤが強調表示されるだけです。これをオンにしましょう。これらのスクリーンショットと例はすべて、このドキュメントの作成時点で最新の Chrome Canary(Chrome 27)のものです。

図 1: 単一レイヤのページ

<!doctype html>
<html>
<body>
  <div>I am a strange root.</div>
</body>
</html>
<ph type="x-smartling-placeholder">
</ph> 合成レイヤがページのベースレイヤを囲む境界線をレンダリングするスクリーンショット
合成レイヤがページのベースレイヤを囲む境界線をレンダリングするスクリーンショット

このページではレイヤが 1 つしかありません。青いグリッドはタイルを表しています。タイルは、Chrome が大きなレイヤの一部を GPU に一度にアップロードするために使用するレイヤのサブユニットと考えることができます。ここではそれほど重要ではありません。

図 2: 自身のレイヤ内にある要素

<!doctype html>
<html>
<body>
  <div style="transform: rotateY(30deg) rotateX(-30deg); width: 200px;">
    I am a strange root.
  </div>
</body>
</html>
<ph type="x-smartling-placeholder">
</ph> 回転したレイヤのレンダリング境界のスクリーンショット
回転したレイヤのレンダリング境界のスクリーンショット

回転する <div> に 3D CSS プロパティを設定すると、要素が独自のレイヤを取得したときにどのように表示されるかを確認できます。このビューでは、レイヤの枠線を表すオレンジ色の枠線に注目してください。

レイヤ作成基準

他に独自のレイヤを取得できるものはあるでしょうか。ここでの Chrome のヒューリスティックは時間とともに進化し、現在も進化していますが、現在、以下のトリガーレイヤ作成に対応しています。

  • 3D または視点変換の CSS プロパティ
  • <video> 要素にアクセラレーテッド動画デコードを使用しています
  • 3D(WebGL)コンテキストまたはアクセラレーテッド 2D コンテキストの <canvas> 要素
  • 合成プラグイン(Flash など)
  • 不透明度の CSS アニメーションを使用した要素、またはアニメーション化された変換を使用した要素
  • 高速化された CSS フィルタを使用した要素
  • 要素に合成レイヤがある子孫がある(つまり、要素に独自のレイヤにある子要素がある場合)
  • 要素に、合成レイヤ(つまり合成レイヤの上にレンダリングされる)を持つ Z-Index の低い兄弟要素があります

実践的な意味: アニメーション

レイヤをあちこちに移動することもできるため、アニメーションで非常に便利です。

図 3: アニメーション レイヤ

<!doctype html>
<html>
<head>
  <style>
  div {
    animation-duration: 5s;
    animation-name: slide;
    animation-iteration-count: infinite;
    animation-direction: alternate;
    width: 200px;
    height: 200px;
    margin: 100px;
    background-color: gray;
  }
  @keyframes slide {
    from {
      transform: rotate(0deg);
    }
    to {
      transform: rotate(120deg);
    }
  }
  </style>
</head>
<body>
  <div>I am a strange root.</div>
</body>
</html>

前述したように、レイヤは静的なウェブ コンテンツ上を移動する際に非常に便利です。基本的なケースでは、Chrome はレイヤのコンテンツをソフトウェア ビットマップにペイントしてから、GPU にテクスチャとしてアップロードします。そのコンテンツが今後変更されない場合は、再ペイントする必要はありません。これは良いことです。再ペイントには、JavaScript の実行などの他の作業に時間を費やすことができる時間がかかります。また、ペイントが長いと、アニメーションが途切れたり遅延したりします。

たとえば、こちらの Dev Tools タイムラインのビューをご覧ください。このレイヤが前後に回転している間、ペイント オペレーションはありません。

<ph type="x-smartling-placeholder">
</ph> アニメーション中の Dev Tools タイムラインのスクリーンショット
アニメーション中の Dev Tools タイムラインのスクリーンショット

無効です。再ペイント

レイヤのコンテンツが変更された場合は、再ペイントする必要があります。

図 4: レイヤの再ペイント

<!doctype html>
<html>
<head>
  <style>
  div {
    animation-duration: 5s;
    animation-name: slide;
    animation-iteration-count: infinite;
    animation-direction: alternate;
    width: 200px;
    height: 200px;
    margin: 100px;
    background-color: gray;
  }
  @keyframes slide {
    from {
      transform: rotate(0deg);
    }
    to {
      transform: rotate(120deg);
    }
  }
  </style>
</head>
<body>
  <div id="foo">I am a strange root.</div>
  <input id="paint" type="button" value="repaint">
  <script>
    var w = 200;
    document.getElementById('paint').onclick = function() {
      document.getElementById('foo').style.width = (w++) + 'px';
    }
  </script>
</body>
</html>

入力要素がクリックされるたびに、回転する要素が 1 ピクセル幅を広げます。これにより、要素全体(この場合はレイヤ全体)の再レイアウトと再ペイントが発生します。

ペイントされた領域を確認するには、Dev Tools の [Rendering] という見出しの下にある Dev Tools の [Show Paint rects] ツールを使用すると便利です。オンにすると、アニメーション要素とボタンの両方が赤色で点滅します。

<ph type="x-smartling-placeholder">
</ph> [ペイント範囲を表示] チェックボックスのスクリーンショット
[ペイント範囲を表示] チェックボックスのスクリーンショット

ペイント イベントは Dev Tools タイムラインにも表示されます。鋭い目の読者は、ペイント イベントが 2 つあることに気付くかもしれません。1 つはレイヤで、もう 1 つはボタン自体で、ボタン自体が押された状態または押された状態から変化したときに再ペイントされます。

<ph type="x-smartling-placeholder">
</ph> デベロッパー ツール タイムラインでのレイヤの再ペイントのスクリーンショット
デベロッパー ツール タイムラインでのレイヤの再ペイントのスクリーンショット

なお、Chrome は必ずしもレイヤ全体を再ペイントする必要はありません。DOM の中で無効化された部分のみを再ペイントするようにしています。この場合、変更した DOM 要素はレイヤ全体のサイズです。しかし、その他にも多くの DOM 要素が 1 つのレイヤに含まれます。

次の明らかな問題は、無効化の原因と再ペイントの強制です。無効化を強制する可能性のあるエッジケースは多数あるため、網羅的に答えるのは困難です。最も一般的な原因は、CSS スタイルを操作するか再レイアウトを引き起こすことで DOM が汚れてしまうことです。Tony Gentilcore は再レイアウトの原因に関するブログ投稿を執筆しています。また、Stoyan Stefanov はペイントについて詳しく説明した記事を公開しています(ただし、最後には描画だけであり、この複雑な合成作業ではありません)。

作業中のものに影響があるかどうかを判断する最善の方法は、開発ツールの [Timeline] ツールと [Show Paint Rects] ツールを使用して、再描画が必要でないときに再描画しているかどうかを確認し、再レイアウト/再描画の直前に DOM が汚れた場所を特定することです。ペイントは避けられないけれども長時間かかると思われる場合は、Dev Tools の連続ペイント モードに関する Eberhard Gräther の記事をご覧ください。

まとめ: DOM から画面へ

Chrome はどのようにして DOM を画面の画像に変換するのでしょうか。概念的には、

  1. DOM を取得して複数のレイヤに分割する
  2. これらの各レイヤを個別にソフトウェア ビットマップにペイントする
  3. それらをテクスチャとして GPU にアップロードする
  4. さまざまなレイヤを合成して、最終的な画面画像にします。

これは、Chrome でウェブページのフレームが初めて生成されるときに行う必要があります。しかし、そうすれば、将来のフレームのためにいくつかの近道できます。

  1. 特定の CSS プロパティが変更されても、何も再描画する必要はありません。Chrome は、GPU にすでに存在している既存のレイヤをテクスチャとして再合成するだけで、合成プロパティが異なります(配置や不透明度などが異なるなど)。
  2. レイヤの一部が無効になった場合は、再ペイントと再アップロードが行われます。コンテンツは同じままでも、合成された属性が変化した場合(翻訳された場合や不透明度が変化する場合など)、Chrome はコンテンツを GPU に残したまま再合成して新しいフレームを作成できます。

ここで明らかなように、レイヤベースの合成モデルはレンダリング パフォーマンスに深く影響します。何も描画する必要がない場合、合成は比較的安価であるため、レンダリングのパフォーマンスをデバッグする場合は、レイヤの再ペイントを避けることが全体的な目標として適しています。経験豊富な開発者は、上記の合成トリガーのリストを見ると、レイヤの作成を簡単に強制できることに気付くでしょう。ただし、あえて作成しても、無料ではないので注意してください。システムの RAM と GPU のメモリを占有し(特にモバイル デバイスでは制限されます)、多数の関数があると、可視領域を追跡するロジックに他のオーバーヘッドが生じる可能性があります。また、多くのレイヤでは、レイヤが大きく、以前は見られなかった部分と重なっている場合に、ラスタライズの時間が長くなることがあります。このような状況は「オーバードロー」とも呼ばれます。知識を賢く活用してください。

今回のご案内は以上です。レイヤモデルの実用的な影響については、今後いくつかの記事で解説しますので、ご期待ください。

参考情報