本セッションの登壇者
セッション動画
今回はブラウザのレンダリングの大まかな仕組みから、CSSアニメーションのパフォーマンスを予測できるようになるというお話をしたいと思います。実際にChromeで計測した数値をまじえながら、使用するプロパティによってどれだけパフォーマンスの差が生まれるのか、なぜ違いが生まれるのかを解明し、最後に未来のお話にも触れていきます。
株式会社ピクセルグリッドでフロントエンドエンジニアをやっておりますtomixyがお送りします。私のGitHubのリポジトリでは、アニメーションのパフォーマンス計測結果を解析するスクリプトを公開しておりますので、あわせて見ていただけたら嬉しいです。
CSSの描画手順とアニメーションの原理
ここから先はブラウザで表示されたWebサイトを1枚のデジタル絵画に見立てて考えてみます。
そもそもアニメーションとは完成された絵画の一部を破壊し、新しく書き換えることを瞬時に何度も繰り返して行うことです。
絵を描くことは、下書き、ペン入れ、色塗り、あとは別レイヤで用意した部品を重ねる合成処理など、いくつかの段階を経て行われます。下書きからやり直すと当然時間がかかります。
このCSSプロパティの値を変えたときに、絵画がどこまで壊れるか、どの段階からやり直さなければならないかを知ることで、アニメーションのパフォーマンスを予測できるようになります。
CSSが解釈された後にWebページが描かれていく過程についてですが、だいたいは人間が絵を描く手順と同じです。
まずはレイアウトです。各要素の位置と大きさを決めて、要素のボックスを並べていきます。
位置と構成が決まったら、次はペイントです。実際に輪郭を描いたり、色を塗ったりしていきます。
最後にレイヤーで分けて描いていた各部品を合成して完成です。レイアウト段階で適用されるプロパティはいずれも値を変更すれば周囲の要素の位置や大きさにも影響が及ぶものです。ここで登場したプロパティをアニメーションさせると、ドミノ倒しのように周辺のパーツが壊れてしまい、広範囲にわたって下書きからすべてやり直すことになります。
ペイント段階で適用されるのは、各要素に閉じたスタイルなので、値を変えたときにレイアウトをやり直す必要はありません。しかし、領域内のピクセルひとつひとつを塗りつぶす処理が走ることになるので、ペイントはレンダリングの中でも最も時間がかかります。
ペイント処理が必要になるプロパティの中でもとくに重いものがbox-shadowです。影は無数のピクセルによって構成されていますが、そのピクセルごとにぼかし半径の分だけ周囲のピクセルを調べて色を混合する処理が発生します。
ぼかし半径が大きければ大きいほど、膨大な数のピクセルを調べることになるので、当然重くなります。また、border-radiusと併用すると、曲線を滑らかにするためにさらにピクセルを細分化して濃淡を付ける処理が発生するため、ますます重くなります。box-shadowをアニメーションさせたいときは、あらかじめ疑似要素にbox-shadowを設定しておいて、その疑似要素のopacityをアニメーションさせるとよいでしょう。
また、たくさんの要素に影をつけたい場合には、影をSVGやCSSでブラウザに描画させるのではなく、画像として用意するようにします。
最後のステップはレイヤーの合成です。ブラウザは要素を適宜、ペイントレイヤーと呼ばれる別のレイヤーに振り分けて描画し、これらのレイヤーを最後に重ね合わせることによって最終的な画面を構築します。
GPUにレンダリングを外注するハードウェアアクセラレーション
ところで、アニメーションでCPUに負荷がかかると、CPUがほかに行うべきJavaScriptなどの処理が進まなくなってしまうことがあります。そこで一定の条件を満たすペイントレイヤーは、GPUにレンダリング処理を外注するようにします。このような特別なレイヤーを合成レイヤーと呼び、GPUに処理を外注することをハードウェアアクセラレーションと言います。
たとえばtransformをアニメーションさせた場合、一時的に合成レイヤーが生成されます。まずtranslateさせたい要素だけを別のレイヤーに書き出し、それをGPUに送信します。レイヤーにはtranslateさせる要素しか描画されておらず、その他の部分は透明です。なので、レイヤーごとちょっとずらして重ね合わせるだけで、移動したように見せることができます。透明フィルムを上から貼り付けるようなイメージです。
仮にtransformではなくtopやleftをtransitionさせると、レイアウト自体が壊れてしまいます。ブラウザは影響範囲をチェックし、再レンダリングは壊れた箇所だけに止めようとしますが、それでもペイント処理は広範囲に及びます。
再レンダリング処理にかかった時間を計測すると、topとtransformではこれほどの違いが生まれます。transformを使った場合は別レイヤーに書き出すときにしかペイント処理が発生しないので、再レンダリングがかなり短い時間で済むことがわかります。
さてレイヤーの合成だけで済ませることで、処理はたしかに軽くなりますが、レイヤーの存在自体は非常に重くメモリを大量に消費するものです。わざと3Dの指定を入れたり、will-changeプロパティを指定することで、レイヤーを無理やり生成させる手法はスマートフォンではむしろ負荷が大きくなるリスクがあります。iOSのChromeではGPU処理を伴うtransform、opacityなどをトランジションさせた際にフレームレートが落ちて瞬時にアニメーション後の表示になってしまうバグに遭遇することもあります。とはいえこのバグも解決の未来は近そうです。
さて、現時点ではopacityとtransformだけでアニメションを実装することが推奨されていますが、Chromiumは近い将来、新たにbackground-colorとclip-pathをハードウェアアクセラレーションの対象に追加するという声明を出しています。background-colorはtransform、opacityに次いでアニメーションでよく使われているプロパティで、clip-pathはアニメーションの幅をぐっと広げるものです。
clip-pathによるアニメーションの実装
最後にclip-pathによるアニメーション実装の考え方を簡単に紹介したいと思います。clip-pathは指定した図形の範囲内しか見えないようにペイント範囲を切り抜くプロパティです。
insetでは、boxの端からの距離を指定することで四角形に切り抜くことができます。距離の指定方法はpaddingやmarginと同様です。
たとえばbox全体が見えなくなるように100%のinsetを指定しておいて、inset(0 0 0 0)
まで遷移させると、スライドアニメーションを実現できます。
とはいえ現状のclip-pathはペイント処理が重めなので、今後に期待しましょう。
またinsetではborder-radiusと同様の記法で、roundキーワードの後に角を丸くする指定を加えることもできます。
たとえばfilterプロパティによるぼかしと併用する際は、border-radiusよりもclip-pathで角丸を表現した方が圧倒的にパフォーマンスが良くなります。
ちなみにborder-radiusはぼかし範囲がボーダーの外側まで及びますが、clip-pathによる角丸では要素外に「にじみ」がはみ出すことはありません。
clip-pathのpolygonでは、頂点の座標を列挙することで、自由に多角形で切り抜くことができます。座標の原点は要素のボックスの左上の角です。
たとえば、すべての頂点に同じ座標を指定すると点になるので、そこからどんな図形にも変化させられます。
円で切り抜くこともでき、うまく使えば波紋アニメーションを簡単に実現できます。
楕円も使えます。
とはいえ、transformやopacityが不要になるわけではなく、組み合わせることで表現の幅がぐっと広がるのではないでしょうか。
まとめ - 未来はもう少しマシになる…かも
アニメーションはとくにスマートフォンでは思うように動かないことも多いと思います。そこにはパフォーマンス上の問題が潜んでいることが多いです。
パフォーマンス改善に特効薬はなく、地道に分析して試行錯誤するしかありませんが、原因の目星を付ける上でちょっと役立つ考え方と、もう少し自由になれるかもしれない未来の可能性をご紹介しました。
ご清聴、ありがとうございました。