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 と、RenderLayers のサブツリーを担当する GraphicsLayers です。ここで最も興味深いのは後者です。GraphicsLayers はテクスチャとして GPU にアップロードする要素です。ここからは単に GraphicsLayer と呼ぶことにします。

GPU の用語はさておき、テクスチャとは何でしょうか。これは、メインメモリ(RAM)からビデオメモリ(GPU 上の VRAM)に移動するビットマップ画像と考えてください。GPU に読み込んだら、これをメッシュ ジオメトリにマッピングできます。ビデオゲームや CAD プログラムでこの技術を使用して、3D の骨格モデルに「肌」を与えることができます。Chrome は、テクスチャを使用してウェブページ コンテンツのまとまりを GPU に書き込みます。テクスチャは、非常にシンプルな長方形のメッシュに適用することで、安価にさまざまな位置や変形にマッピングできます。これが 3D CSS の仕組みであり、高速スクロールにも適しています。ただし、この両方の点については後ほど詳しく説明します。

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

Chrome でレイヤを調べる際に非常に便利なツールは、Dev Tools の設定(小さな歯車アイコンなど)にある [レンダリング] の見出しにある [合成レイヤの枠線を表示する] フラグです。レイヤが画面上のどこにあるかを簡単にハイライト表示します。オンにしましょう。これらのスクリーンショットと例はすべて、この記事の作成時点における最新の Chrome Canary 版 Chrome 27 から取得したものです。

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

<!doctype html>
<html>
<body>
  <div>I am a strange root.</div>
</body>
</html>
ページのベースレイヤを囲む合成レイヤのレンダリング境界のスクリーンショット
ページの基本レイヤを囲む合成レイヤのレンダリング境界のスクリーンショット

このページにはレイヤが 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>
回転したレイヤのレンダリング境界のスクリーンショット
回転したレイヤのレンダリング境界のスクリーンショット

回転させる <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 タイムラインのビューをご覧ください。このレイヤが前後に回転している間、ペイント オペレーションはありません。

アニメーション中のデベロッパー ツールのタイムラインのスクリーンショット
アニメーション中のデベロッパー ツールのタイムラインのスクリーンショット

無効です。再ペイント

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

図 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 ピクセル広くなります。これにより、要素全体(この場合はレイヤ全体)の再レイアウトと再描画が発生します。

何が描画されるかは、デベロッパー ツールの [Show Paint rects] ツールで確認できます。これは、Dev Tools の設定の [Rendering] 見出しの下にも配置されています。オンにした後、ボタンがクリックされるとアニメーション要素とボタンの両方が赤色に点滅します。

[ペイント四角形を表示する] チェックボックスのスクリーンショット
[Show Paint rects] チェックボックスのスクリーンショット

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

デベロッパー ツールのタイムラインによるレイヤの再描画のスクリーンショット
デベロッパー ツールのタイムラインによるレイヤの再描画のスクリーンショット

なお、Chrome は必ずしもレイヤ全体を再描画する必要はなく、無効化された DOM の一部のみを再描画するようにします。この場合、変更した DOM 要素はレイヤ全体のサイズになります。しかし、多くのケースでは、1 つのレイヤに多数の DOM 要素があります。

次に明らかな疑問は、無効化の原因と再描画を強制する要因です。無効化を強制する可能性のあるエッジケースは数多く存在するため、これを網羅的に答えるのは困難です。最も一般的な原因は、CSS スタイルの操作や再レイアウトの原因による DOM のダーティ化です。Tony Gentilcore が再レイアウトの原因に関するブログ投稿を投稿し、Stoyan Stefanov がペイントについて詳しく説明した記事を公開しています(ただし、最後はペイントのみで終わり、この高度な合成作業ではありません)。

現在の作業に影響しているかどうかを把握する最良の方法は、デベロッパー ツールの [タイムライン] ツールと [ペイント四角形を表示] ツールを使用して、必要ではないときに再ペイントしているかどうかを確認し、再レイアウト/再ペイントの直前に DOM が汚れた場所を特定することです。ペイントは避けられないものの、かなり時間がかかっていると思われる場合は、デベロッパー ツールの連続ペイント モードに関する Eberhard Gräther の記事をご覧ください。

まとめ: DOM からスクリーンへ

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

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

これらはすべて、Chrome でウェブページのフレームが初めて生成されるときに行う必要があります。しかし、そうすると、将来のフレームでいくつかのショートカットが必要になる可能性があります。

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

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

以上です。レイヤモデルの実践的な影響については、今後の記事にご期待ください。

参考情報