パフォーマンスに優しいCSSアニメーションとは

ピクセルパイプライン

スクリーンに要素が見えるためには、ピクセルパイプラインという過程を経ている。

この投稿では特に PaintCompositeに焦点を当てることになる。

なめらかなアニメーションを実現するために

Webはレンダリングを繰り返すことで要素の可視化を実現している。今日のほとんどの端末は、画面を1秒に60回リフレッシュする。つまり、60fpsはレンダリングのパフォーマンスが良い状態と言える。
従来のブラウザでは、レンダリングはCPUが担っていた。現在ではGPUを使用することでレンダリングパフォーマンス向上が行われている。
つまり、CPUだけでなく、GPUもうまく活用することが、60fpsを実現することの鍵になる。

CPU( Central Processing Unit)はコンピューターのブレイン。
かつてはシングルコアで、処理を一つずつ処理していたが、最近はマルチコアで、並列に処理できるようになった。

GPU(Graphics Processing Unit)はグラフィック処理のため開発され、単純なタスクの処理に向いてる。しかし近年は処理性能が上がっている。

では、CPUが稼働している状態、GPUが稼働している状態とはどういうことだろうか。

Main ThreadとCompositor Thread

最近のブラウザには通常、2つの重要な実行スレッドがある。それが、Main ThreadとCompositor Thread。

Main Thread

  • JSの実行
  • HTML要素のCSSスタイルの計算
  • レイアウト
  • 要素を1つ以上のビットマップにペイント
  • これらのビットマップをCompositor Threadに渡す

Compositor Thread

  • GPUを介して画面にビットマップを描画
  • メインスレッドに、ページの表示部分または間もなく表示される部分のビットマップを更新するように要求
  • ページのどの部分が表示されているかを把握
  • スクロールしているときにすぐに表示される部分を把握
  • スクロールするときにページの一部を移動

これら2つのスレッドの関連性は、このページの画像がわかりやすい。

ピクセルパイプラインだと、Layout、PaintはMain Threadが、
CompositeはCompositor Threadが担っている。
レンダリングした結果をスクリーンに描画する時点でCPUもGPUも活用している。
ではもっとパフォーマンスに良いアニメーションを実行するにはどうすればよいのか。
アニメーションさせるプロパティの違いで考える。

positionでのアニメーション

<div id="target"></div>
#target {
  background-color: red;
  width: 100px;
  height: 100px;

  position: absolute;
  left: 0;
  top: 0;
  animation: anim 1s;
}

@keyframes anim {
  from {
    left: 0;
  }
  to {
    left: 5px;
  }
}

これを行った場合のPaintの処理内容を見る。

position

position-layer-summary

Paintが繰り返し(14回)発生し、Repaintしている。
Repaintするということは、Main threadの稼働、つまりCPUがその都度稼働してしまうということ。
CPUで処理がされている間ペイントの処理が止まってしまうので、アニメーションがカクつく場合が生じてしまう。
positionと同時に使うようなleftプロパティはHTMLElement.offsetParentで返ってくるような要素との相対位置を知る必要がある。leftの値を変える度にoffsetを知る必要があるので、都度Layoutを再計算する。
そのためCPUが働くということになる。

transformでのアニメーション

#target {
  background-color: red;
  width: 100px;
  height: 100px;
  animation: anim 1s;
}

@keyframes anim {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(5px);
  }
}

transform

transform-layer-summary

positionと比べてPaintの量が減った。
この結果はレイヤーが生成されたことが関わってくる。

レイヤー

レイヤーはブラウザが生成する。

  1. DOMを取得したブラウザはレイヤーを生成する
  2. 各レイヤーのコンテンツをビットマップにPaintする
  3. そのビットマップをCompositor Threadと同期する
  4. ビットマップをGPUのメモリに送る
  5. ビットマップをGPUメモリからスクリーンに描画する
    5の時、GPUは各レイヤーを合成して描画する。

レイヤーが生成される条件は以下の通り

  • 3D or perspective transform CSS properties
  • <video> elements using accelerated video decoding
  • <canvas> elements with a 3D (WebGL) context or accelerated 2D context
  • Composited plugins (i.e. Flash)
  • Elements with CSS animation for their opacity or using an animated transform
  • Elements with accelerated CSS filters
  • Element has a descendant that has a compositing layer (in other words if the element has a child element that’s in its own layer)
  • Element has a sibling with a lower z-index which has a compositing layer (in other words the it’s rendered on top of a composited layer)

条件見るならこのあたりも見てみるのもいいかも

レイヤーが生成されたら、各レイヤー別に、コンテンツをビットマップにPaintする。
特定のCSSプロパティが変更された場合、GPUの特別な機能でビットマップを更新し、それを描画する。つまり、Main Threadを動かす必要が最初の一回のレイヤーを生成する箇所だけで済むので、CPUに優しくなる。

実際にレイヤーが生成されたか確認する

chromeのLayersパネルで確認可能。
アニメーションが終わった時点でレイヤーは確認できなくなるので、アニメーションを反復させるようにして確認する。

#target {
  background-color: red;
  width: 100px;
  height: 100px;

  position: absolute;
  left: 0;
  top: 0;
  animation: anim 1s ease-in-out 0s infinite alternate;
}

@keyframes anim {
  from {
    left: 0;
  }
  to {
    left: 5px;
  }
}

position-layer

positionの場合、document以外にレイヤーは生成されない。

#target {
  background-color: red;
  width: 100px;
  height: 100px;
  animation: anim 1s ease-in-out 0s infinite alternate;
}

@keyframes anim {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(5px);
  }
}

transform-layer

transformの場合、transformを適用した要素にレイヤーが生成された。

結論

パフォーマンスに優しいアニメーションを行うためには、
レイヤーが生成されるcssプロパティを用いて、GPUによる再描画を行ってもらい、Main threadによるLayoutの計算、Paintの再実行が行われないように、CPUを極力稼働させないようにすることが肝になる。
しかし、GPUには、実はビットマップをGPUメモリに送る作業が苦手という面もある。つまり、レイヤーを多数作り、GPUメモリへのアクセスを多用すると、GPUのパフォーマンスも落ちる。
なので、Repaintが多い箇所だけGPUに任せるなど、バランスを保つことも重要になる。

参考