Firefoxでアニメーションがガクガクしたときの対応記録

# 事象

Firefoxにおいて、GSAPを用いてSVG要素をY軸方向にtransformでアニメーションさせたとき、SVG画像が細かく拡縮するような感じでガクガクと揺れるようにアニメーションしました。（animation-timing-functionのsteps()のようなイメージ）

opacityを0から1に変化させながら、下から上にアニメーションさせて、スライドインしてきているように見せるアニメーションでした。

- 環境
  - MacBook Pro (16-inch, 2019)
  - macOS Monterey 12.4
  - Firefox 102.0.01 (64ビット)
- opacityを変化させなくても事象が発生した
- Y軸方向の移動量を上下させても事象が発生した
- X軸方向の移動でも（Y軸に比べると少々視認しづらかったが）事象が発生していた
- イージングを他のものにしても事象が発生した
- GSAPでは移動中はtranslate3dを用いている

# 結論

**アニメーションさせる要素に`transform: rotate(0.0001deg)`を付与する。**

# 解決までの道のり：失敗編

*忙しい人は読み飛ばしてください*

## `will-change`が効かない…だと…

このようなアニメーション系の問題が発生したときは、まず事前にアニメーションさせる要素をブラウザにお伝えしてみて要素を見ます。

現在普及している方法は`will-change`プロパティ、これを使わない手はありません。

```css
.animation {
  will-change: transform;
}
```

しかし、これは**効果がありませんでした**。

「ハードウェアアクセラレーションが有効になってない…とか…？」と思い、設定画面でハードウェアアクセラレーションを明示的に有効にしてみましたが、変わりませんでした。

![Firefox Performance Setting](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924363/67afaed6-d962-7329-df76-f9cd0361e03f.png)

## 古のtranslateZハック

**効果なし**

`translateZ`、もしくは`translate3d`を用いることでGPUに合成レイヤーを作らせて、ハードウェアアクセラレーションを実現する古のハックがありますが、こちらも効果なしでした。

```css
.animation {
  transform: translateZ(0);
}
```

## matrix3d()を使ってみる

**効果なし**

ちょうどその頃、別件ではありますが、友人が実装したというページを見せてもらったときに、`matrix`でアニメーションさせていることに気づきました。

「なんでtranslateじゃなくてmatrixなん？」と聞いてみると、「TweenMaxの仕様だよ」と。

GSAP統合前のTweenMaxでは、滑らかなアニメーションを最適化するために、遷移中は`matrix`を使用するとのこと。

> TweenMaxではアニメーションを極力軽く、なめらかにしようとする働きがある。
> y5pxに行くまでの間はtranslate3Dを使用し、5pxになった瞬間matrixを使用する仕様になっている
> [出典: TweenMaxでXやYを使ってアニメーションした時、アニメーション中に半ピクセルずれる不具合について
](https://reiwinn-web.net/2017/12/15/tweenmax%E3%81%A7x%E3%82%84y%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%97%E3%81%9F%E6%99%82%E3%80%81%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC/)

なめらかにねぇ〜ふ〜ん、ということで、モノは試しでやってみました。

と思ったら、GSAPで`matrix`を使おうとしても`translate`に自動的に変換されてしまい検証できませんでしたので、一旦CSSのanimationでやってみました。

```css
@keyframes slide-in {
  from {
    transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 50, 0, 1);
  }

  to {
    transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
  }
}
```

が、やっぱり何も効果はありませんでした。

## width heightを設定してみる

**効果なし**

HTML上で`img`タグに対して`width`と`height`属性に値を入れてみました。
レイアウトシフト対策として入れられることが多く、何となく効果があるかもと思ってやってみましたが、特に効果はありませんでした。

---

# 解決までの道のり：発見編

## 画像のレンダリングサイズが読み込んだサイズと違う？

ブラウザの検証ツールを見ていると、画像のレンダリングサイズと読み込んだ画像のサイズが違うことに気がつきました。

CSSのwidthの値の記述ミスで、widthを画像サイズと同じ値に固定にしてみると、ガタガタがなくなりました。

しかしながら、同じ事象が起きていた他の要素を確認すると、レンダリングサイズと読み込みサイズが同じなのにガタガタしていました。

つまり、`width: 300px`はスムーズにアニメーションするけど、`width: 301px`はダメみたいなことになっていたのです。

…ということは、レンダリングサイズと読み込みサイズが問題なのではなく、実際レンダリングされる要素の**ピクセル値自体が問題**ということです。

これにより大きなヒントを得ました。

## サブピクセルレンダリングが問題なのでは？

サブピクセルとは、1px未満、つまり小数点以下のピクセルのことです。

Firefoxの今回の問題の場合、`y: 0px`の位置から`y: 1px`に遷移アニメーションさせたいとき、`translateY()`には0〜1pxの間の小数値ピクセルが適宜入力されて移動しているように見えますが、この小数値が**整数値もしくはそれの近似値で計算される**ことで、結果的に大雑把に動いているように見えているのではないかと考えました。

## Bugzilla先生で関係しそうなissueを発見

Firefoxのバグトラッカーサイト、Bugzillaで以下のようなissueを発見しました。

https://bugzilla.mozilla.org/show_bug.cgi?id=739176

タイトルは以下です。

> No sub-pixel rendering while transitioning translate transforms
>
> translate transformの遷移中にサブピクセルレンダリングが発生しない

うーん、めっちゃこれっぽい…。
2022年7月現在から見ると、11年前に立てられたスレッドですが、5ヶ月前に更新があり、11ヶ月前にはリプライもついており、未だに存在するバグ（仕様）のようです。

スレッドでは、**ピクセルスナップ**[Pixel Snapping]という単語が出てきていました。
整数値にスナップする機能が効いていて、これが特にアニメーションに悪影響を及ぼしているようです。

直近のリプライではこのスレ主が以下のように語っています。

> This issue has gotten worse over the years. Firefox now smoothly transitions parts of an element, but not all of it.
>
> この問題は、ここ数年悪化しています。現在Firefoxは、一部要素はスムーズに遷移しますが、全てではありません。

今回はこの方がおっしゃる"全てではない"要素に該当してしまったようです。

---

# 解決までの道のり：解決編

*スレッドに多くのヒントが残されていました。*

## 一つの解答： rotate(0.0001deg)

さて、それでは解決法ですが、結論から言うと私は`transform: rotate(0.0001deg)`で直りました。

```css
.animation {
  transform: rotate(0.0001deg);
}
```

> When we rotate, we have to resample the buffer and things necessarily get blurry.

rotateを使うとバッファを再サンプリングするらしいのですが、その際に要素がボケるようです。
ボケることでピクセルスナップが無効になり、スムーズにアニメーションする、ということだと理解しました。

ちなみに、rotateさせる角度は関係なさそうで、1degでもスムージングされました。とにかくrotateしてピクセルスナップさせないことが肝のようです。

注意点が2点あり、まず、**完全にかくつきがなくなるわけではありません**。
効果は絶大ですが、やらないよりはマシくらいに思っておいた方が良さそうです。
そしてもう1点は、回転させることでデザインと異なってしまうことは忘れてはいけません。

Firefoxで発生する問題なので、CSSハックか何かでFirefoxにのみ適用されるプロパティにしておくと良いかもしれません。

## rotateで直らない場合は？

私はrotateで直りましたが、テキストだとそれで解決しなかった方もいるようで、その方は以下のようにして直したと仰っています。

```scss
.animation {
  transform: translate3d(0,0,0);
  filter: blur(0px);
}
```

なるほど、これもblurによってぼかしているということのようです。
「`blur(0px)`ならボケないだろ」と思うのですが、これは経験則として、値が0でもテキスト要素にfilterを適用するとボケます。テキストに対するアンチエイリアスとかが関係しているんでしょうか（投げかけ）

※`blur(0)`と`blur(0px)`が同じ挙動なのか未確認のため、スレッドに記載の通り`blur(0px)`と記述しています。

また、表示するデバイスピクセルによっても変わるとのことで、「ピクセルが細かくなればなるほど気にならなくなる」とのことです。検証はしていませんが、ピクセルスナップが原因とすれば、これは納得できますね。

# まとめ

Firefoxのアニメーションのスムージングについて、知らなかった挙動だったので振り返られるようにまとめてみました。

:::note
- **遷移させる要素をぼかすとスムージングされているように見える**
  - `rotate(0.0001deg)`を使う
    - 回転させているので、デザインと異なることは留意する
  - `filter: blur(0px)`を使う
    - 恐らくテキスト要素に対して有効
:::

その他、知見がある方は是非ご教授ください。

# 参考

https://bugzilla.mozilla.org/show_bug.cgi?id=739176