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 です。後者は、GraphicsLayer がテクスチャとして GPU にアップロードされるため、ここでは最も興味深いものです。以降、GraphicsLayer を「レイヤ」と呼びます。

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

レイヤの概念を示すために、いくつかの例を見てみましょう。

Chrome でレイヤを調べるときに非常に便利なツールは、DevTools の設定(小さな歯車アイコン)の [レンダリング] 見出しにある「合成レイヤの境界を表示」フラグです。画面上でレイヤが配置されている場所を簡単にハイライト表示します。オンにしましょう。これらのスクリーンショットと例は、執筆時点での最新の 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>
回転したレイヤのレンダリング境界のスクリーンショット
回転したレイヤのレンダリング境界のスクリーンショット

3D CSS プロパティを <div> に適用して回転させると、要素が独自のレイヤを取得したときにどのように見えるかを確認できます。このビューのレイヤの輪郭を示すオレンジ色の境界線に注目してください。

レイヤ作成の条件

他にどのようなものが独自のレイヤを取得しますか?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 の実行など他の作業に充てることができます。ペイントに時間がかかると、アニメーションのヒッチや遅延が発生します。

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

アニメーション中の DevTools タイムラインのスクリーンショット
アニメーション中の DevTools のタイムラインのスクリーンショット

無効です。塗り直し

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

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

ペイントされている内容を確認するには、DevTools の [ペイント領域を表示] ツールを使用します。このツールは、DevTools の設定の [レンダリング] の見出しの下にあります。オンにすると、ボタンがクリックされたときに、アニメーション要素とボタンの両方が赤色で点滅します。

[ペイント矩形を表示] チェックボックスのスクリーンショット
ペイント領域を表示するチェックボックスのスクリーンショット

ペイント イベントは、DevTools のタイムラインにも表示されます。よく見ると、2 つのペイント イベントがあることに気づくでしょう。1 つはレイヤ用で、もう 1 つはボタン自体用です。ボタンは押された状態と押されていない状態を切り替える際に再描画されます。

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

Chrome は、必ずしもレイヤ全体を再描画する必要はなく、無効になった DOM の部分のみを再描画するようにしています。この例では、変更した DOM 要素はレイヤ全体のサイズです。ただし、多くの場合、レイヤには多くの DOM 要素が含まれます。

次に疑問になるのは、無効化の原因と強制再描画の原因です。無効化を強制するエッジケースが多数あるため、この質問に網羅的に回答するのは難しいです。最も一般的な原因は、CSS スタイルを操作したり、再レイアウトを発生させたりして DOM を汚染することです。Tony Gentilcore は、再レイアウトの原因に関する優れたブログ投稿を公開しています。Stoyan Stefanov は、ペイントについて詳しく説明している記事を公開しています(ただし、この高度な合成については触れていません)。

作業中のものに影響しているかどうかを判断する最善の方法は、DevTools のタイムライン ツールとペイント レクタの表示ツールを使用して、不要なときに再描画が行われていないかを確認し、その再レイアウト/再描画の直前に 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 のメモリを消費します(特にモバイル デバイスでは制限されます)。また、作成しすぎると、表示されるオブジェクトを追跡するロジックにオーバーヘッドが発生する可能性があります。レイヤが大量にある場合、レイヤが大きく、以前は重複していなかった部分で重複が多く発生すると、ラスタライズにかかる時間が実際に長くなることもあります。この状態は「オーバードロー」と呼ばれることがあります。この知識を賢く活用してください。

説明は以上です。レイヤモデルの実用的な影響については、今後の記事で詳しく説明します。

参考情報