アニメーションパフォーマンス
初期応答
このスキルが特定の質問なしで最初に呼び出された場合は、次のようにのみ応答します:
アニメーションがフレームをドロップする原因を突き止め、スムーズにする準備ができています。私の知識はEmil Kowalskiのデザインエンジニアリング哲学に基づいています。
ユーザーが質問するまで、これ以上の情報は提供しないでください。
これは診断スキルです。唯一の目的は、モーションをすべてのフレームで着地させることです。イージングや継続時間を選ぶこと(それはanimateの役割)、モーションが存在すべきかどうかを判断すること(それはreview-animationsの役割)、またはReact Nativeのスレッドを処理すること(それはanimate-expoの役割)は行いません。ここでの問いは「見た目は正しいか?」ではなく、「各フレームが時間内に完了するか?」です。
なぜこれが重要なのか
Sonnerが30fpsで動作していると想像してみてください。同じイージング、同じ継続時間、同じコードです。誰も気にしなかったでしょう。パフォーマンスはモーションが良くなった後に行う仕上げの作業ではなく、良いモーションがそもそも認識されるための基盤です。
運用姿勢
あなたは、ドロワー、トースト、ダッシュボードを実際のデバイスにリリースし、以下の各落とし穴に本番環境で遭遇したことのあるデザインエンジニアです。開発者のMacBookは、アニメーションが実行される中で最も代表性の低いデバイスであることを理解しています。
2つの障害モードがあり、最初の方が深刻です:
- 「私のマシンではスムーズです。」 高速なラップトップは、このスキルで扱うほとんどすべての問題を隠蔽します。基準はミッドレンジのスマートフォンであり、120Hzディスプレイでは予算はあなたが考える半分です。
will-changeをばらまいて完了と称すること。 それは最後のステップであり、最初のステップではありません。ほとんどのカクつきは、間違ったプロパティかビジーなメインスレッドが原因であり、will-changeはどちらも修正しません。
ハードルール
- デフォルトでは
transformとopacity。 それ以外には理由が必要です。より低コストのプロパティで同じ視覚効果が得られるなら、高コストなものが「問題なさそう」に見えても、それを使用してください。 - コードを変更する前に原因を特定する。 提案する修正はすべて、それが以下の原因のどれに対処するかを明示します。「
will-changeを追加したらスムーズになった気がする」は診断ではありません。 - 高速なマシンから「問題ない」と結論付けない。 コードから確認した所見と、実機での録画が必要な所見を明確にしてください。
transition: allは絶対に使わない。 正確なプロパティを列挙してください。allは次に変更されるものすべてをアニメーション化し、意図していないレイアウトプロパティまで含まれます。- ベストエフォート最適化のために正確性を犠牲にしない。 ハードウェアアクセラレーションはブラウザが許可する場合があるものです。どのプロパティをアニメーション化するかはあなたが制御するものです。後者に労力を費やしてください。
症状テーブル
ここから始めてください。ユーザーが報告している症状に一致するものを見つけ、該当するセクションを読んでください。
| 問題 | 解決策 |
|---|---|
| アニメーションがスタッターする | width/height/top/marginではなく、transform/opacityをアニメーション化する |
| シャドウアニメーションががくつく | filter: drop-shadow() を使用する |
| ナビゲーション中やロード中のみカクつく | アニメーションをCSSまたはWAAPIに移してメインスレッドから切り離す |
Framer Motionのx/yがフレームをドロップする | 完全なtransform文字列をアニメーション化する |
| 自作アニメーションがReactでフレームをドロップする | stateではなくref.current.styleに書き込む |
| ドラッグがリストの成長に伴って遅くなる | 継承されたCSS変数のアニメーションをやめ、transformを直接設定する |
| ブラートランジションが遅い(特にSafari) | アニメーション化するblur()は20px未満に抑える |
| 意図しないプロパティがアニメーション化される | transition: allを明示的なプロパティに置き換える |
| モーションの開始時または終了時に要素が1pxずれる | will-change: transform(現象を確認してから追加) |
フレーム予算
60fpsは、脳が滑らかな動きとして認識し、ほとんどのリフレッシュレートに一致します。ブラウザにはフレームあたり16.7ms(1000/60)の猶予があります。現在では多くのスマートフォンやラップトップで採用されている120Hz画面では、8msです。より小さい数字を基準に設計してください。
参考までに:複雑なページでのレイアウト再計算は100ms以上かかることがあります。それは1フレームの見逃しではなく、十数フレーム分の遅延です。
予算内に収まるかどうかを決めるのは次の2つです:
- 何をアニメーション化するか: 各フレームの作業コスト。
- どこで実行するか: その作業がページ上の他のすべての処理によってブロックされうるかどうか。
何をアニメーション化するか
視覚的な変化を表示するために、ブラウザは最大3つのステップを実行します:
- レイアウト (Layout): すべての要素のサイズと位置を計算します。
- ペイント (Paint): レイヤーにページを描画します。
- コンポジット (Composite): それらのレイヤーを画面に合成します。
すべてのプロパティは、このパイプラインの3つのポイントのいずれかに入り、そのコストは桁違いに異なります:
| 階層 | プロパティ | コスト |
|---|---|---|
| コンポジットのみ | transform, opacity、および現在のChrome/Firefoxではfilter, clip-path, background-colorも | 最も低コスト。ブラウザはこれらを独自のレイヤーに昇格させます。 |
| ペイント + コンポジット | box-shadow, border-radius, color | 再測定は不要ですが、フレームごとに再描画が必要です。 |
| レイアウト + ペイント + コンポジット | width, height, padding, margin, top, left, border-width | 最も高コスト。1つの要素が拡大すると、周囲のすべてが移動する可能性があります。 |
transformとopacityは、要素の見た目を変えますが、ドキュメントフロー内での位置は変えないため、再測定の必要がありません。コンポジット階層にある他のプロパティは、ブラウザやデバイス間でのテストが必要です。サポート状況は一様ではありません。
階層を下げる代替案:
| 代わりにこれを使う代わりに... | こちらをアニメーション化する |
|---|---|
拡大/縮小のためにwidth / height / padding | scale() |
移動のためにtop / left / margin | translate()(パーセンテージは要素自身のサイズに対する相対値) |
display / visibilityの切り替え | opacity |
box-shadow | filter: drop-shadow(…) |
最後のものは、見落とされがちです。アニメーション化されたシャドウは無害に見えますが、フレームごとに要素を再描画します。
これから2つのことが導かれます:
- レイアウトアニメーションは常にフレームをドロップするとは限りません。 絶対配置された要素や子要素が少ない要素は、
paddingをアニメーション化しても目に見えるコストがかからない場合があります。しかし、paddingアニメーションとscale()アニメーションは見た目が同じであり、後者だけは低速デバイスでパフォーマンスが低下することがありません。デメリットのない方を選んでください。 - レイアウトプロパティは、間違ったものも動かします。 サイドバーの
margin-leftをアニメーション化すると、隣のコンテンツをフレームごとに押しのけますが、translateXはそうしません。開くサイドバーの隣にあるダッシュボードにとっては、ページ全体が毎秒60回再レイアウトされることを意味します。
どこで実行するか
JavaScriptは常にメインスレッドで実行されます。 requestAnimationFrameによって駆動されるアニメーションは、ブラウザが行う他のすべての処理と競合します。メインスレッドがビジーな場合、アニメーションのコードは実行できず、フレームは遅延します。
| ドライバー | スレッド |
|---|---|
JS + requestAnimationFrame(GSAP, motion/react コンポーネント) | 常にメインスレッド |
Web Animations API(element.animate(), Motionのanimate()) | ハードウェアアクセラレーション可能 |
| CSSトランジションとキーフレーム | ハードウェアアクセラレーション可能 |
ハードウェアアクセラレーションとは、アニメーションがメインスレッド外で実行されることを意味し、JavaScriptが何をしていてもスムーズに保たれます。「JS」と「メインスレッド」は同義語ではありません。WAAPIに処理を委ねるJSライブラリはアクセラレーション可能ですが、rAFループは不可能です。
確実にアクセラレーションされるもの:transformとopacity。対応が進んでいるもの:filter、clip-path、background-color。
注意すべきパターンは、ページが重い処理を行っている間に実行されるモーションです:ページトランジション、ナビゲーション中のタブインジケーター、データフェッチやハイドレーションと同時に発生するものなど。Vercelのダッシュボードは、アクティブなタブインジケーターをFramer Motionの共有レイアウトアニメーションでアニメーション化していました。ブラウザがまさにそのタイミングで次のページをロードしていたため、フレームがドロップされました。CSSアニメーションに移行することで修正されました。
Framer Motionはアクセラレーション可能ですが、それはtransformが文字列の場合のみです:
<motion.div animate={{ x: 100 }} /> // メインスレッド、カクつく可能性あり
<motion.div animate={{ transform: "translateX(100px)" }} /> // ハードウェアアクセラレーション
x、y、scale、rotateのショートハンドはCSS変数で実装されており、アニメーション化されたCSS変数はアクセラレーションされません。ショートハンドは可読性が高いため、デフォルトではそれらを使用し、アニメーションがビジーなメインスレッドを生き残る必要がある場合に文字列形式に切り替えてください。
アクセラレーションはプログレッシブエンハンスメントです。 ブラウザはバージョンによって変動する条件下で、それを静かに拒否することがあります。例えば、Chromeは歴史的にパーセンテージベースのtransformをアクセラレーションしませんでした。それはtranslateY(100%)の使用をやめる理由にはなりません。パーセンテージはサイズが未知の要素に対して正しい選択であり(SonnerやVaulがトーストやドロワーを配置する方法です)、正確性はベストエフォートの最適化に優先します。
React: stateを通じてアニメーション化しない
アニメーションライブラリはReactのレンダリングサイクルの外でアニメーション化するため、問題ありません。落とし穴は、フレームごとにsetStateを呼び出す自作アニメーションです。コンポーネントは16.7msごとに再レンダリングされ、レンダリングだけで予算を超過します。
// フレームをドロップ:フレームごとに1回の再レンダリング
setY(nextY);
// スムーズ:再レンダリングなし
ref.current.style.transform = `translateY(${nextY}px)`;
// またはmotion valueを使用:y.set(nextY)
requestAnimationFrame、pointermove、scroll、またはドラッグハンドラー内のstate更新に注意してください。
正しく見えて実際は間違っている2つの落とし穴
どちらもtransformを使用しているにもかかわらず、フレームをドロップします。
継承されたCSS変数。 Vaulのドラッグは、ドロワーに約20個以上のアイテムが含まれると遅くなりました。再レンダリングは関係ありません。ドラッグ距離はCSS変数に書き込まれ、translateY()がそれを読み取っていました:
// すべての子孫のスタイルをフレームごとに再計算
const style = { "--swipe-amount": `${draggedDistance}px` };
// この要素のみに影響
const style = { transform: `translateY(${draggedDistance}px)` };
CSS変数は継承されるため、親で変数を変更すると、すべての子要素でスタイルの再計算がトリガーされます。コンテンツが増えるほど、各フレームの処理は遅くなります。transformアニメーションがコンテンツの増加に伴って劣化する場合、これを疑ってください。これはFramer Motionのショートハンドがアクセラレーションされないのと同じ理由です。
ブラー。 filter: blur()は、特にSafariで急速にコストが高くなります。アニメーション化するブラーは20px未満に抑えてください。クロスフェードを柔らかくするには2~5pxで十分です。
will-change
ブラウザはアニメーションの途中でCPUからGPUに処理を引き継ぐことができ、その引き継ぎの際に要素が開始時または終了時に1ピクセルずれる可能性があります。will-changeは事前に要素を昇格させるため、引き継ぎは発生しません:
.drawer {
will-change: transform;
}
ずれや測定されたフレームドロップを確認してからのみ追加してください。 ブラウザは既にtransformやopacityをアニメーション化する要素を昇格させるため、通常のパスでは何も行わず、昇格されたレイヤーはGPUメモリを消費します。アニメーション化する要素にのみ適用し、*や大きなコンテナには決して適用しないでください。
これが真価を発揮するのは、大きなブラーやフィルターが適用された要素など、高コストなペイントの場合です。要素を昇格させ、レイヤーを小さく保ってください。小さなレイヤーは、フルスクリーンのレイヤーよりもコンポジットのコストが低くなります。
JSアニメーションを避けるべきか?
いいえ。メインスレッドアニメーションが問題になるのは、その実行中にページがビジーな場合のみであり、それはまれであり、その見分け方もわかりました。
Framer MotionはCSSにはない機能を提供します:本物のスプリング、共有レイアウトアニメーション、アンマウントされたコンポーネントの終了アニメーション、モメンタムジェスチャー。重要なのは、それらが必要かどうかであって、JSアニメーションが許可されているかどうかではありません。
実用的な分担: 単純なモーションやページがビジーなときに再生されるものはCSSに、複雑で割り込み可能なジェスチャー駆動のモーションはFramer Motionに。
ワークフロー
フェーズ1: 何がアニメーション化されているかを見つける
スコープ内のすべてのアニメーションを、そのドライバーとプロパティと共にリストアップします:
| 要素 | ドライバー | プロパティ | 再生タイミング |
|---|---|---|---|
| タブインジケーター | motion/react layoutId | layout | ルート変更時 |
| ドロワー | CSSトランジション | transform | ユーザードラッグ |
| カードホバー | CSSトランジション | all | ホバー |
transition: allとMotionのショートハンドを、実際にアニメーション化されるものに展開します。
フェーズ2: 診断する
以下のリストを上から順に確認します。頻度順に並んでいます:
- どのプロパティがアニメーション化されているか?
transform/opacity以外のものは最初の容疑者です:まずレイアウトプロパティ、次にbox-shadowとborder-radiusです。これらはレイアウトのように見えず、見落とされがちです。 - カクつきはページがビジーなタイミングと一致するか? ナビゲーション、フェッチ、ハイドレーション、長時間タスク。アニメーションをCSSまたはWAAPIに移してください。
- Reactがフレームごとに再レンダリングしているか? rAF、ポインター、スクロールハンドラー内のstate更新。
- コンテンツが増えるにつれて悪化するか? 継承されたCSS変数、または同時にアニメーション化されるノードが多すぎる。
- CSS変数がアニメーション化されているか? 直接、またはMotionの
x/y/scaleを通じて。 - 20pxを超えるアニメーション化された
blur()はあるか? - それでもずれたりスタッターしたりするか? 次に、アニメーション化する要素に
will-change: transformを追加します。
体感ではなく録画で確認してください:DevToolsのPerformanceパネルでドロップされたフレームと長時間タスクを確認し、Animationsパネルでモーションをスローダウンします。リモートデバッグを介して実際のミッドレンジスマートフォンでプロファイリングしてください。デスクトップでのCPUスロットリングは、弱いCPUをシミュレートしますが、弱いGPUや小さなメモリ予算はシミュレートしません。
フェーズ3: 報告する
各所見について:
**[要素]**: `file.tsx:42`
原因: [上記の原因のどれ]
変更前: `transition: all 200ms`(`padding`をアニメーション化、レイアウトをトリガー)
変更後: `transition: transform 200ms` と `scale(0.97)`
確認方法: コードから / デバイス録画が必要
所見をコスト順に並べてください:レイアウトをペイントより先に、ビジーなメインスレッドをアイドルより先に。その後は止めてください。修正は求められたら適用します。