GPUアクセラレーションとposition: relativeによるレイヤー生成について

またアニメーション...

ボタンなどのUIにGPUアクセラレーションが効いたアニメーションをつけたとき、iOSにおいてはiPhone4か4SのWebViewあたり、Androidにおいては….まぁ機種依存的(げんなり)に、アニメーションの立ち上がりが遅いことがあります。

その辺を調査していたところ、position: relativeの指定による、意図しないレイヤー生成&GPUアクセラレーション巻き込みによって、何かしら合成レイヤー周りでオーバーヘッドが発生してしまっているんではないかな、という憶測に行き着いた次第。今回はその辺りを見ていきます。

GPUアクセラレーションが効いたアニメーションは、CSS Animations、CSS Transitionsのほか、特殊なプロパティ(transform3d: scale(1,1)とか)で強制的にGPUアクセラレーションを効かせたCanvasアニメーションも含まれます。

2013/04/10: 大まかにWebKitの話と考えてもらえればよいと思います。WebViewかどうかはあまり関係ないはずです。

SafariのDebugメニュー

何はともあれ合成レイヤーを見るには、Safari(Mac)のDebugが便利なので、シェルから下記のコマンドで有効にします。今回、使用したのはv6.0.3 (7536.28.10)でした。

% defaults write com.apple.Safari IncludeInternalDebugMenu -boolean true 

これを有効にしたあとSafariを起動しなおして、Debug → Drawing/Compositing Flags → Show Compositing Bordersにチェックが入っていれば、合成レイヤーの生成を確認できます。番号がついているレイヤーは、GPUに送られていると考えてほぼ問題ないと思います。

Safariでいいの?という疑問はあるかもしれませんが、今回の話に限ってはiOS/Android実機の挙動と比べても差異がなく十分な感じです。気になる人はiOS SimulatorのColor Blended Layersや、ChromeのCompositing Bordersも合わせて見ると良いかも。

1. ありがちな状態

下記がスタート地点のコードです。position: relativeを数カ所に入れていますが、これらのプロパティは、このサンプルにおいての必要性はさておき、一般的レイアウトで使用されがちなパターンを再現したものです。

サンプルでは、アクティビティのフィードのようなUIをイメージしつつLikeボタンを置いています。Likeをクリックするとアニメーション(検証用に10sとってます)が実行されて、そのときの合成レイヤーを確認できるようになっています。


早速Debugを有効にしたSafariでサンプルを開きます(画像にリンクが貼ってあります)

これはひどい。広い範囲が合成対象のレイヤーとして認識されています。

実際に、冒頭で上げたようなスペックが高くないiOS端末や、ヤンチャなAndroid端末ではアニメーションの実行にモタつきやチラつきが見られます。


恐らくposition: relativeによってレイアウト用のレイヤーが生成されていて、かつ同じレベルにそのようなレイヤーが複数あると、その中のひとつのレイヤーでCSS Animationsが動作する際に、それらのレイヤーが丸ごとGPUアクセラレーションに巻き込まれて合成対象になっている感じでしょう。

本来、高速に描画処理をできるGPUにデータを送ることでアニメーションを滑らかにするはずのところ、無駄なデータをGPUに送りすぎていることで、データ転送時のオーバーヘッドのほうがボトルネックになってしまっています。


2. 応急処置 z-indexで持ち上げる

まずは応急処置のパターン。たとえばLikeボタンを他とは関係ないレイヤーとしてz-indexプロパティで持ち上げてしまいます。

.container button {
  position: relative;
  z-index: 5;
  /* …略... */
}

合成の適用範囲は減ったので、GPUへのデータ転送コストは減ったように見えます。実機でチェックするとチラつきやモタつきに対して、一定の改善が見られるはずの状態です。

しかし、Likeボタン同士は結局同じレイヤーにあるので、他のコンテナのLikeボタンは巻き込まれたままであることが分かります。


3. position: relativeの根絶

そして最も安全そうなパターンとして、position: relativeを根絶してみます。今回のサンプルでは、単純にコメントアウトしてみます。

.container {
  /* position: relative; */
  /* …略... */
}
.container div {
  /* position: relative; */
  /* …略... */
}
.container button {
  /* position: relative; */
  /* …略... */
}

position: relativeが指定されていた箇所をすべてコメントアウトした結果、意図しないレイヤー生成が行われなくなり、押したLikeボタンのみがGPU合成されるレイヤーになりました。

この状態になると、アニメーションの立ち上がりもスムーズで、チラつきもほとんど無くなってきます。


ボタンUIを例に挙げましたが、最初に示したような広範囲の巻き込みは、translate系を利用したフリック操作系のUIでも発生していることが少なくありません。

アニメーション系の動きやパフォーマンスに気になる部分があれば、まずは今回のような巻き込みが発生していないかを確認し、必要に応じてできる限り巻き込み範囲を減らすようにすると良さそう。


結論、GPUこわい

ということで、position: relativeによるレイヤー生成が、GPUアクセラレーションを伴うアニメーションのパフォーマンスに影響する例を紹介してみました。本文中ではiOS/Androidにフォーカスしていましたが、モバイルほどシビアでないにせよPCでも共通の問題を抱えています。

今回は説明しやすいパターンで極端に例示しましたが、このサンプルをベースに各所のCSSプロパティをいじって見てみると、おおよそのパターンが掴めるはずです。

ビジュアルデザインのためということで、CSSはメンテナンス性と引き換えに力技で書かれることも少なくないと思います。一方、今回のケースのように意図しないところでパフォーマンスと引き換えしてしまっているパターンも知っておくと、より精度の高いスタイルを記述できるのではないでしょーか。


追記

今回のサンプル、実機で見たときのチラつきとかモタつきまではうまく再現できていないっぽいです。もう少し、表示要素なりページ内コンテンツなりがリソースを喰らう現実的な環境まで追い込まないと、そこまでは再現できないかも。

参考

今更ですが、GPU合成されるレイヤー、という日本語で合ってるのだろうか...。compositing border自体は合成の境界線を示してるだけ感ある。まあ、うん、雰囲気でお願いします。