はじめに
アニメーションのパフォーマンス最適化テクニックは世の中に溢れてい��す。例えば:
-
transform、opacityのみを変更し、他のプロパティは触らない(リフローを避けるため) -
アニメーション要素に
transform: translate3d(0, 0, 0)、will-change: transformなどを適用してハードウェアアクセラレーションを有効にする -
アニメーション要素は
position: fixed、absoluteで定位し、リフローを避ける -
アニメーション要素に高い
z-indexを適用し、コンポジットレイヤーの数を減らす -
その他有用なルール
問題は:これらのルールを慎重に守っているのに、なぜアニメーションはまだカクカクしたりフレーム落ちしたりするのか?最適化はまだできる?どこから手をつければ?
一.ハードウェアアクセラレーションは非標準的
The most important thing I'd like to tell you before we dive deep into GPU compositing is this: It's a giant hack. You won't find anything (at least for now) in the W3C's specifications about how compositing works, about how to explicitly put an element on a compositing layer or even about compositing itself. It's just an optimization that the browser applies to perform certain tasks and that each browser vendor implements in its own way.
多くの場合、ハードウェアアクセラレーションを有効にすると確かにパフォーマンスが向上しますが、この部分は非標準的です。W3C にはコンポジットの仕組みや、要素をコンポジットレイヤーに明示的に配置する方法、あるいはコンポジットそのものについての仕様はありません。これはブラウザが特定のタスクを実行するために適用する最適化であり、各ブラウザベンダーが独自に実装しているものです。
つまり、transform: translate3d(0, 0, 0) などのテクニックでハードウェアアクセラレーションを有効にするのは、仕様外の行為であり、パフォーマンスが向上する可能性もあれば、深刻なパフォーマンス問題を引き起こす可能性もあります
将来的に仕様が整備されるかもしれませんが、それまでは、各項のパフォーマンス最適化原則を遵守するだけでなく、実際のレンダリングフローを考慮し、原理からパフォーマンス問題を解決する必要があります。
ハードウェアアクセラレーション(Hardware Acceleration)
CSS アニメーションにおけるハードウェアアクセラレーションとは GPU コンポジット(GPU compositing)のことで、ブラウザが CPU で直接画像データを生成して表示するのではなく、関連レイヤーのデータを GPU に送信し、GPU は画像データ演算に長けているため、加速とみなされます。
では、ハードウェアアクセラレーションが利用できない場合、ブラウザはどうやってページをレンダリングするのか?
在没有硬件加速的情况下,浏览器通常是依赖于 CPU 来渲染生成网页的内容,大致的做法是遍历这些层,然后按照顺序把这些层的内容依次绘制在一个内部存储空间上(例如 bitmap),最后把这个内部表示显示出来,这种做法就是软件渲染(software rendering)
二.transform と opacity の特殊性
以前はレイアウト関連のプロパティを変更してアニメーションを作成していました。例えば:
@keyframes move {
from { left: 30px; }
to { left: 100px; }
}
アニメーションの各フレームごとに、ブラウザは要素の形状と位置を再計算し(リフロー)、新しい状態をレンダリングし(リペイント)、画面に表示します。
ページ全体のリフローとリペイントは遅いですが、アニメーション要素を前景として抽出し、各フレームで他の部分を背景として変更せず、アニメーション要素のみを再レンダリングし、前景と背景を合成すれば、もっと速くなるのでは?確かに、GPU はサブピクセルレベルのレイヤー合成を高速に行えるからです。
しかし、この前提は、動く部分と動かない部分で前景と背景のレイヤーを分けられることです。アニメーション要素がレイアウトの影響を受けたり、動きの中でレイアウトに影響を与えたりすると、前景と背景の境界が崩れ、単純に 2 つのレイヤーに分けることができなくなります。では、position: fixed | absolute を適用すればレイアウトに影響しないことを保証できるのか?
いいえ、left はパーセンテージ値や相対単位(em、vw など)を受け取れるため、ブラウザはそのプロパティの変化がレイアウトと無関係であることを 100% 確信できないため、単純に前景と背景のレイヤーを分けることはできません。例えば:
@keyframes move {
from { left: 30px; }
to { left: 100%; }
}
しかし、ブラウザは transform と opacity の変化がレイアウトと無関係であることを 100% 確信でき、レイアウトの影響を受けず、その変化が既存のレイアウトに影響を与えることもありません。そのため、これら 2 つのプロパティの特殊性は以下の通りです:
-
does not affect the document's flow,
-
does not depend on the document's flow,
-
does not cause a repaint.
レイアウトに影響せず、レイアウトの影響も受けず、その変化が他の部分のリペイントを引き起こさない場合、それは確実に独立したレイヤーとして抽出でき、GPU に安心して処理させ、ハードウェアアクセラレーションのメリットを享受できます:
-
繊細(GPU はサブピクセルレベルの精度を実現でき、GPU にとって負担にならない)
-
滑らか(計算集約的な JS タスクの影響を受けず、アニメーションは GPU に委ねられ、CPU とは無関係)
三.GPU コンポジットの代償
It might surprise you, but the GPU is a separate computer. That's right: An essential part of every modern device is actually a standalone unit with its own processors and its own memory- and data-processing models. And the browser, like any other app or game, has to talk with the GPU as it would with an external device.
GPU は独立した一部であり、独自のプロセッサ、メモリ、データ処理モデルを持っています。つまり、CPU でメモリ内に作成された画像データを GPU と直接共有することはできず、パッケージ化して GPU に送信する必要があり、GPU はそれを受け取ってから期待される一連の操作を実行できます。このプロセスには時間がかかり、データのパッケージ化にはメモリが必要です。
必要なメモリ量は以下に依存します:
-
コンポジットレイヤーの数
-
コンポジットレイヤーのサイズ
数よりもコンポジットレイヤーのサイズの影響の方が大きいです。例えば:
.rect {
width: 320px;
height: 240px;
background: #f00;
}
この赤いブロックを GPU に送信する場合、必要なストレージスペースは:320 × 240 × 3 = 230400B = 225KB(RGB は 3 バイト必要)。画像に透明部分がある場合、320 × 240 × 4 = 307200B = 300KB が必要です。
このように目立たない小さな赤いブロックでも 2、300KB 必要で、ページには数十から数百の要素があり、画面の半分や全体を占める要素も少なくありません。これらすべてをコン���ジットレイヤーとして GPU に委ねると、メモリ消費は想像に難くありません。そのため、一部の極端なハードウェアアクセラレーションのシナリオではパフォーマンスが非常に悪くなります:
[caption id="attachment_1251" align="alignnone" width="303"]
gpu compositing issue[/caption]
1GB RAM のデバイスの場合、システムとバックグラウンドプロセスで 1/3、ブラウザと現在のページで 1/3 が消費され、実際に使えるのは 200〜300MB だけです。コンポジットレイヤーが多すぎたり大きすぎたりすると、メモリが急速に消費され、フレーム落ち(カクつき、ちらつき)が発生し、場合によってはブラウザ/アプリケーションがクラッシュすることも十分にあり得ます。
P.S.詳細は CSS3 硬件加速也有坑!!! を参照
四.コンポジットレイヤーの作成
ブラウザはいくつかの状況でコンポジットレイヤーを作成します。例えば:
-
3D transforms: translate3d, translateZ and so on;
-
<video>, <canvas> and <iframe> elements;
-
animation of transform and opacity via Element.animate();
-
animation of transform and opacity via СSS transitions and animations;
-
position: fixed;
-
will-change;
-
filter;
-
。。。
他にもたくさんあります。詳細は CompositingReasons.h で定義された定数を参照
これらはほとんどが期待通りの明示的に作成されたコンポジットレイヤーですが、他の状況でもコンポジットレイヤーが作成されます:
- コンポジットレイヤーの上にある要素もコンポジットレイヤーとして作成される(B の
z-indexが A より大きく、A にアニメーションを適用すると、B も独立したコンポジットレイヤーに押し込まれる)
これは理解しやすいです。アニメーションの過程で A と B が重なる可能性があり、B に隠される可能性があるため、GPU は各フレームで A レイヤーにアニメーションを適用し、その後 B レイヤーと合成して正しい結果を得る必要があります。そのため、B は无论如何でもコンポジットレイヤーに押し込まれ、A と一緒に GPU に委ねられる必要があります。
暗黙的にコンポジットレイヤーが作成されるのは主に重なりを考慮してのことです。ブラウザが重なるかどうか確信できない場合、不確かなものをすべてコンポジットレイヤーに押し込む必要があります。そのため、この観点から見ると、高い z-index という原則には理があります。
五.ハードウェアアクセラレーションの長所と短所
長所
-
アニメーションが非常に滑らかで、60fps に達する
-
アニメーションの実行プロセスは独立したスレッド内であり、計算集約的な JS タスクの影響を受けない
短所
-
要素をコンポジットレイヤーに押し込む際に追加のリペイントが必要で、場合によっては遅い(ページ全体のリペイントが必要な場合もある)
-
コンポジットレイヤーのデータを GPU に転送する際に追加の時間がかかり、コンポジットレイヤーの数とサイズに依存する。これにより、ミドル〜ローエンドデバイスでちらつきが発生する可能性がある
-
各コンポジットレイヤーは一部のメモリを消費し、モバイルデバイスではメモリが高価で、過剰な占有はブラウザ/アプリケーションのクラッシュを引き起こす可能性がある
-
暗黙的なコンポジットレイヤーの問題があり、注意しないとメモリが急増する
-
文字がぼやけたり、要素が変形したりすることがある
最も重要な問題はメモリ消費とリペイントに集中しているため、アニメーションのパフォーマンス最適化の目標はメモリ消費を減らし、リペイントを減らすことです。
六.パフォーマンス最適化テクニック
1.暗黙的なコンポジットレイヤーを避ける
コンポジットレイヤーはリペイントとメモリ消費に直接影響します:アニメーション開始時にコンポジットレイヤーを作成し、終了時に削除すると、リペイントが発生します。アニメーション開始時にレイヤーデータを GPU に送信する必要があるため、メモリ消費はここに集中します。2 つの提案:
-
アニメーション要素に高い
z-indexを適用し、bodyの子要素として直接配置する。深くネストされたアニメーション要素の場合は、body下にコピーを作成し、アニメーション効果の実装にのみ使用する -
アニメーション要素に
will-changeを適用する。ブラウザはこれらの要素を事前にコンポジットレイヤーに押し込み、アニメーションの開始/終了時をより滑らかにできるが、乱用してはならない。不要になったらすぐに削除し、メモリ消費を減らす
2.transform と opacity のみを変更する
transform、opacity を使用できる場合は優先的に使用し、使用できない場合は方法を考えて使用する。例えば、背景色のグラデーションは、上にある疑似要素の背景色の opacity アニメーションでシミュレートできる。box-shadow のアニメーションは、下にある疑似要素の opacity アニメーションでシミュレートできる。これらの曲がりくった実装方法は、顕著なパフォーマンス向上をもたらす。
3.コンポジットレイヤーのサイズを減らす
小さな要素を拡大表示する場合、width、height を減らし、GPU に渡すデータを減らし、GPU で scale して拡大表示する。視覚的な違いはない(単色背景の要素に多く使用され、重要度の低い画像も 5% から 10% の幅と高さの圧縮が可能)。例えば:
<div id="a"></div>
<div id="b"></div>
<style>
#a, #b {
will-change: transform;
background-color: #f00;
}
#a {
width: 100px;
height: 100px;
}
#b {
width: 10px;
height: 10px;
transform: scale(10);
}
</style>
最終的に表示される 2 つの赤いブロックは視覚的に違いはないが、メモリ消費を 90% 削減できる。
4.子要素のアニメーションとコンテナのアニメーションを考慮する
コンテナのアニメーションには不要なメモリ消費が存在する可能性がある。例えば、子要素間の隙間も GPU に有効なデータとして送信される。各子要素に個別にアニメーションを適用すれば、この部分のメモリ消費を避けられる。
例えば、12 本の太陽光線が回転する場合、コンテナを回転させるとコンテナの画像全体を GPU に送信するが、12 本の光線を個別に回転させると光線間の 11 本の隙間がなくなり、メモリを半分節約できる。
5.コンポジットレイヤーの数とサイズを早めに���目する
最初からコンポジットレイヤー、特に暗黙的に作成されたコンポジットレイヤーに注目し、後からの最適化がレイアウトに影響するのを避ける。
コンポジットレイヤーのサイズは数よりも影響が大きいが、ブラウザはいくつかのコンポジットレイヤーを 1 つに統合する最適化操作を行う。これをLayer Squashingと呼ぶ。しかし、場合によっては 1 つの大きなコンポジットレイヤーの方がいくつかの小さなコンポジットレイヤーよりも多くのメモリを消費するため、必要に応じてこの最適化を手動で無効にできる:
// 各要素に異なる translateZ を適用
translateZ(0.0001px), translateZ(0.0002px)
6.ハードウェアアクセラレーションを乱用しない
何も問題がないのに transform: translateZ(0)、will-change: transform などのハードウェアアクセラレーションを強制的に有効にするプロパティをむやみに追加しない。GPU コンポジットには欠点と不足があり、非標準的な行為である。最良の場合は顕著なパフォーマンス向上をもたらし、最悪の場合はブラウザをクラッシュさせる可能性がある。
コメントはまだありません