最適なWebアニメーションを実装したい

# この記事について
## 背景
みなさんはフェードイン／フェードアウトアニメーションを実装する時はどのように実装しますか？
- CSSの```transition```プロパティや```animation```プロパティで```opacity```プロパティを変化させる
- jQueryを使用していれば```fadeIn()```／```fadeOut()```メソッドを使用する

など、色々方法が出てくるかと思います。ではどの方法が最適でしょうか？

筆者は普段レガシー環境でエンハンス業務に従事していますが、なんの気なしに既存コードを流用して、後者のjQueryのメソッドを使ってアニメーションを実装しようとしていました。そこで先輩エンジニアに「jQueryのアニメーションを使うとCPUで動くからパフォーマンスがちょっと悪い、できればGPUで動くCSSアニメーションを使いたい」の一言をいただき、Webアニメーションの奥深さを知りました。

## 伝えたいこと
この記事では、先輩エンジニアの一言から筆者が調べたことをまとめています。
> ではどの方法が最適でしょうか？

この問いに答えられるような、Webアニメーションを実装する際に一つの観点を与えるものになっていればと思います。

# CPU処理とGPU処理
ここで冒頭の先輩コメントに登場した、CPU処理とGPU処理について簡単に特徴をまとめます。
## CPU
- CPU（Central Processing Unit）とは、コンピューターの制御・計算（演算）を行う中央演算処理装置。コア数により扱える処理の数が増えるが、GPUよりは少ない。大量の処理を行うのには向かない。一つの複雑な処理を行うのに向いている
- JavaScriptやjQueryアニメーションはCPU上で動作する

## GPU
- GPU（Graphics Processing Unit）とは、画像処理装置のことでCPUに比べてコア数が多く、並列作業が得意。定型的で、膨大な処理を行うのに向いている
- CSSアニメーションの特定のプロパティは、GPU上で動作する。CPUで動くアニメーションよりも滑らかに動作する

:::note warn
__GPUで動くCSS3プロパティについて__
```transition```プロパティや```animation```プロパティで設定した全てのプロパティがGPU上で動作するわけではなく、```transform```や```opacity```などの一部プロパティが対象になります。
詳しくは後述の[CSSプロパティのコスト](#cssプロパティのコスト)を参照。
:::

# CSSプロパティのコスト
再レンダリングのフローやレイヤの概念を知ることで、なぜGPU処理の方がパフォーマンスがよいのか理解できます。
## 要素の再レンダリングフロー
アニメーション時において、要素が再レンダリングされる際は、以下のフローが発生します。
  1. style（スタイル再計算）
  2. layout（要素の配置・リフロー）
  3. paint（描画・リペイント）
  4. composite（レイヤ合成）

1と4は必ず行われますが、GPU上で動くプロパティは2と3の工程を省くことができ、低コストとなるためスムーズなアニメーションを実現できます。

:::note info
__GPUのレイヤ概念__
GPUではレイヤ概念というものがあり、レイヤを作成、作成したレイヤごと移動させる（アニメーションさせる）ことで、2と3の工程を省いています。
:::

まとめると、
- CPUレンダリングの場合：要素の位置が変更される度にpaint処理が走る
- GPUレンダリングの場合：レイヤ概念を用いて、レイヤを移動（2（layout）,3（paint）処理を省ける）させて4（composite）が行われる

といった違いがあり、GPUレンダリングの方が低コストな理由がわかります。

## CSSプロパティにより異なるコスト
プロパティの特性により、再レンダリングするコストがそれぞれ異なってきます。
- 全ての過程を辿るため、コストが高いプロパティ
    - ```left```, ```max-width```, ```border-width```, ```margin-left```, ```font-size```など
- 2（layout）の過程は省かれるプロパティ
    - ```color```など
- 1（style）と4（composite）のみのためコストは低いプロパティ
    - ```transform```, ```opacity```など

:::note
GPU上で動作しないCSSプロパティも動作するプロパティと一緒に使えばレイヤにのせられる場合があります。
例えば、```animation```プロパティで```keyframs```を作成する際は、アニメーション対象のプロパティを個別で設定するよりも、合わせて設定した方がパフォーマンスが向上する場合があります。
:::

## レイヤをあらかじめ作成する```will-change```プロパティ
前述のGPUレンダリングの場合レイヤが生成されるタイミングはアニメーション開始の直前になりますが、事前にレイヤを生成しておく方法もあります。あらかじめブラウザに変化することを伝えているため実際に変化があった時にスムーズに動作できます。

https://developer.mozilla.org/ja/docs/Web/CSS/will-change

:::note warn
MDNにも警告が載っていますが、事前にレイヤを作成する方法は、GPUで動作するCSSアニメーションを使ってもカクつく場合など、やむを得ない場合にのみ使用すべきです。乱用すると、事前に大量のレイヤを生成・グラフィック情報保持していることになり、メモリーを消費するためかえってパフォーマンスが悪くなる可能性があります。
:::

:::note
事前にレイヤを作成しておくため、ハック的に```transformZ(0)```プロパティを使う方法もあるようですが、```will-chang```プロパティと同様の理由で、必要ない限りは使用しない方がよいでしょう。
:::

## デベロッパーツールでの確認（Chrome）
再レンダリングはどの処理が走っているのか、レイヤ生成されてるのか、についてはデベロッパーツールから確認することができます。

### 再レンダリングの確認
デベロッパーツール > パフォーマンス > 記録ボタン実行 > イベントログを確認することで、レンダリング処理を確認できます。
![イベントログ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620233/7ee3ba1d-ef36-2eec-bb44-e699da97db39.png)

また、デベロッパーツール > その他のツール > レンダリング > ペイント点滅にチェックするとリペイントされている要素がわかります。以下はレイヤが作成されない```left```プロパティを用いてサンタ画像を左から右へ移動させています。移動の度にリペイントの処理が入っているのがわかります。
![ペイント処理.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620233/6d5d1d24-5aaf-6dfc-b651-0306db2e8850.png)

```scss
.santaImg { // サンタ画像の要素
  position: absolute;
  width: 200px;
  height: auto;
  animation: animation-santa;
  animation-duration: 2s;
  animation-iteration-count: infinite;
}

@keyframes animation-santa {
  0% {
    left: 0;
  }
  100% {
    left: 300px;
  }
}
```


### レイヤ確認
デベロッパーツール > その他のツール > レイヤを選択するとレイヤパネルが開きます。以下の例では```transform: translateX()```でサンタ画像を移動させています。レイヤパネルではページ全体の枠の中にサンタ画像の部分に枠がつき、レイヤになっていることがわかります。
![レイヤパネル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620233/56738bd3-3476-2151-66e9-d7bddf6ebf7e.png)

```scss
.santaImg { // サンタ画像の要素
  width: 200px;
  height: auto;
  animation: animation-santa;
  animation-duration: 2s;
  animation-iteration-count: infinite;
}

@keyframes animation-santa {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(300px);
  }
}
```
このように、レンダリング状況を確認することができます。位置移動のアニメーションを```left```プロパティにした場合は、リペイント処理が走り、レイヤ枠は表示されません。```transform```プロパティの場合にはリペイント処理は走らずに、レイヤ枠が表示されます。
また、```left```プロパティの場合でも```opacity```プロパティと一緒に使用した場合や```will-change```プロパティを使用した場合は、リペイント処理は走らず、レイヤ枠が表示されます。

# Web Animations APIの利用
ここまで「アニメーションは極力GPUで動くCSSアニメーションで実装する方がよい」というような内容を書いてきましたが、時にはJavaScript側で動的な値を取得し、アニメーションをつけたい場合が出てくるかと思います。
こういった時に **Web Animations API**を使用すれば、JavaScript操作と親和性高く、かつ通常のCSSアニメーションと同じように動作するので、GPU側でアニメーションを動作させることが可能です。

https://developer.mozilla.org/ja/docs/Web/API/Web_Animations_API

:::note info
Web Animations APIは外部ライブラリではないため、読み込み負荷かがない点でもパフォーマンス的に優れています。
:::

# まとめ
## パフォーマンスに留意したアニメーションの実装
今回調べたことをもとに、筆者は以下の観点を持つようにしています。

- アニメーション処理はハードウェアアクセラレーション（グラフィック関連の処理をGPUに任せること）を目指す
    - 可能な限り、GPU上で動作するCSSプロパティを使用して実装する
    - GPU上で動作しないCSSプロパティも動作するプロパティと一緒に使えばレイヤに乗せてくれるので、```animation```プロパティの```keyframes```などはあまり細かく区切らずに書く
- JavaScriptから動的な操作が必要な場合はWeb Animations APIなど標準のAPIを使用する

## 参考資料

https://developer.mozilla.org/ja/docs/Web/Performance/Animation_performance_and_frame_rate

https://qiita.com/yuki153/items/9aac0e5c8d7230a7bbe2

https://attadesign.co.jp/blog/816/

https://zenn.dev/silverbirder/articles/b58ba3f8666a57