描画とかGPUアクセラレーションの怪 (モバイル編)

何かと不透明なあたり

気がつけば一ヶ月ほどブログを更新していませんでした。リハビリ記事です。

今回は、GPUを効かせる == それに関連するプロパティ(ただしOSやバージョンによって何がトリガかは厳密に異なる)を適用したら挙動が改善した、というようなノリの体験TIPSをゆるくまとめました。

このあたりの振る舞いについては、描画パフォーマンスの問題として、それなりに明らかになってきているように思います。WebKitのレンダリングプロセスにはじまり、GPU命令のサポートが受けられるのはどんな操作だとか、GPUへ処理が預けられるレイヤーの生成がどーとか、最近よく見聞きします。

自分が業務で扱っているスマートフォン向けのWebサービスでは、このような描画パフォーマンスの問題は、スクロールパフォーマンスと合わせて非常にクリティカルです。この辺りについてのロジカルなまとめは、某氏が近日中にまとめるらしいので、ここではふんわり扱います。

Case1. Canvasアニメーションが重い

ありきたり。初歩?

解決

iOSは-webkit-backface-visibility: hiddenとかを適用すればたぶんOK。Androidは…そもそもCanvasアニメーションが後述の問題(Case3)で見送られていたので不明。

元々Androidはリサイズしなかったら普通に動いていた。2.xは少しFPS低かったかな?去年のデブサミの何かで、AndroidのCanvasは問題多いけど速度面はマシと聞いたようなウロ覚え。よーわからん。

Canvas自体は、本来アニメーションの仕組みをもたないため、タイマーで描画を毎フレームごとに制御するようなノリになります。そのため、最近ならほっといてもGPUアクセラレーションが効くCSS AnimationsやCSS Transitionsと比べると、プレーンな環境ではパフォーマンスに問題が出てしまうことも。

よって、-webtki-backface-visibility: hiddenなどでComposition Layerを強制的に生成させて、GPUに描画処理を送りこむことでパフォーマンスが改善されるようです。

Case2. CSS Animationが妙にチラつく

CSS Animations(いわゆる@keyframesとanimationプロパティ)を使ったUIアニメーションを入れたら、何か異様なチラつきが…。発生条件から逆算したら以下のような状態でした。

<div class="wrapper" style="-webkit-transform-origin: 0px 0px; opacity: 1; -webkit-transform: scale(1, 1)">
  <!-- 中略…4〜5個divなどの要素がネストしてるかんじ -->
  <span class="apply-animation"></span>
  <!-- 中略 -->
</div>

解決

親レイヤーにあたるような要素に-webkit-transform: scale(1, 1)が適用されていたのが原因で、これらを無効化したら解決しました。

自分のケースではZeptoのfx_methodsモジュールが、show/hideを使う度にこれらのプロパティを適用していて、それらが何らか悪さをしていたようです。ちょっとロジカルな原因が説明できないので、なんというかフィーリングで怪しいところ直した類。なんにせよ、transform系が広域のレイヤーに適用されるのは危ないと思います。

特にGPUにレイヤーを送り込む系のプロパティを広域に適用すると、些細な描画の変更が都度GPUで処理されることで、そのオーバーヘッドがパフォーマンスに顕著に影響したりするようです。

Case3. Canvasをwidth, heightでリサイズするとブラウザハング

自分が検証したのは、Galaxy S3ですが、どうにもこうにもAndroid × Canvas の即死ポイントとその回避策 | buccchi.jpでも触れられたように、Android 4.0.x系でCanvas要素をスタイルでリサイズすると即死するようで…。GPUとかはあまり関係ない。

解決

<!-- NG -->
<canvas width="640" height="960" style="width: 320px; height: 480px;"></canvas>

<!-- OK -->
<canvas width="640" height="960" style="-webkit-transform: scale(0.5, 0.5);"></canvas>

このようにしたら、ハングせずに1/2(いわゆるRetina的な対応)のサイズで再生されました。コレについは他の端末でも広域な調査が必要なのですが、ひとまず糸口になったのでは、ということで取り上げた次第。

Case4. iOS6のみ動きが引っかかる

iPhone4 + iOS5でヌルヌル動くフリック操作が、iPhone5 + iOS6で一瞬ひっかかる現象の怪。スペックの問題ではなく実行環境の差異が顕著なケース。

どーも、iOS6 html hardware acceleration changes and how to fix them | indiegamr のOverlapping with other Elementsあたりで説明されている問題に該当していた。

解決

フリック操作を実装した際に、インナー要素をたとえば-webkit-transform: translate3d(-500px, 0, 0)としていたが、、、


<style>
.wrapper {
  width:320px;
  height:240px;
  overflow:hidden;
}
</style>
<div class="wrapper">
  <ul class="inner" style="-webkit-transform: translate3d(-500px, 0, 0)">
    <li><img src="path/to/image.png"></li>
    <li><img src="path/to/image.png"></li>
    <li><img src="path/to/image.png"></li>
  </ul>
</div>

<!-- ▼▼▼▼▼ -->

<style>
.wrapper {
  /*中略*/
}
.applyMagic {
  -webkit-transform: translate3d(0,0,0);
}
</style>

<div class="wrapper applyMagic">
  <ul class="inner" style="-webkit-transform: translate3d(-500px, 0, 0)">
    <li><img src="path/to/image.png"></li>
    <li><img src="path/to/image.png"></li>
    <li><img src="path/to/image.png"></li>
  </ul>
</div>

のようにすることで、iOS6でもアクセラレーションが効いて、iOS5と同じようにヌルっと動くようになった。

iOS6においては、アクセラレーションが適用されているインナー要素と、適用されていないラッパー要素が重なっている(構造的にはラッパー要素の中に入っている)状態で問題が起こっていた。ラッパー要素にもアレゲなプロパティを適用することで、ちゃんとアクセラレーションされるようです。

いやー

JavaScriptの処理がトロい、というよりも先にCSSなりCanvasなりJSなりが密接に絡み合う、描画全般についてのパフォーマンスがクリティカルな要因になったのは良い経験になりました。

今回は触れられなかった、原理にあたる部分をなるべくインプットするようにしていますが、それによってうっかり発生してしまった問題にも勘を働かせて解決できるようになってきました。わかる!わかるぞ!感が楽しい今日この頃。