アニメーションパフォーマンス は何をしますか?
初期応答
このスキルが特定の質問なしで最初に呼び出された場合は、次のようにのみ応答してください。
アニメーションがフレームをドロップする原因を見つけて、滑らかにする準備ができています。私の知識は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は次に変更されるものすべてをアニメーション化し、誰もアニメーション化するつもりのなかったレイアウトプロパティも含みます。- 最善努力の最適化のために正確さを犠牲にしないこと。 ハードウェアアクセラレーションはブラウザが許可する場合があるものです。どのプロパティをアニメーション化するかは、あなたが制御するものです。後者に労力を費やしてください。
症状テーブル
ここから始めてください。ユーザーが見ているものに一致させ、該当するセクションを読んでください。
| 問題 | 解決策 |
|---|---|
| アニメーションがスタッターする | transform/opacityをアニメーション化し、width/height/top/marginは避ける |
| シャドウアニメーションがぎこちない | filter: drop-shadow() |
| ナビゲーション中やロード中のみカクつく | アニメーションをCSSまたはWAAPIに移動してメインスレッド外で実行 |
Motionのx/yがフレームをドロップする | motion 14.2以上にアップグレードするか、完全なtransform文字列をアニメーション化 |
| Reactで自作アニメーションがフレームをドロップする | ref.current.styleに書き込み、stateは使わない |
| リストが大きくなるにつれてドラッグが遅くなる | 継承されたCSS変数のアニメーションをやめ、直接transformを設定 |
| ぼかしトランジションが遅い(特にSafari) | アニメーション化するblur()は20px未満に抑える |
| 予期しないプロパティがアニメーション化される | transition: allを明示的なプロパティに置き換える |
| モーションの開始時または終了時に要素が1pxずれる | will-change: transform、ずれが見えた場合のみ |
フレーム予算
60fpsは、脳が流動的なモーションとして読み取るものであり、ほとんどのリフレッシュレートに一致します。ブラウザに与えられる時間はフレームあたり16.7ms(1000/60)です。120Hzの画面では、現在ほとんどのスマートフォンと多くのラップトップが該当しますが、8msです。より小さい数値に対して設計してください。
規模感: 複雑なページでのレイアウト再計算は100ms以上かかることがあります。それは1フレームの見逃しではなく、十数フレームの見逃しです。
予算内に収めるかどうかを決める2つのこと:
- 何をアニメーション化するか: 各フレームのコスト。
- どこで実行するか: その処理がページで実行されている他のすべての処理によってブロックされる可能性があるかどうか。
何をアニメーション化するか
視覚的な変化を表示するために、ブラウザは最大3つのステップを実行します:
- レイアウト: すべての要素のサイズと位置を計算します。
- ペイント: ページをレイヤーに描画します。
- コンポジット: それらのレイヤーを画面に配置します。
すべてのプロパティは、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のレイアウトアニメーション) | 常にメインスレッド |
Web Animations API(element.animate()、Motionのanimate()およびmotion/reactコンポーネント) | ハードウェアアクセラレーションの可能性あり |
| CSSトランジションとキーフレーム | ハードウェアアクセラレーションの可能性あり |
ハードウェアアクセラレーションとは、アニメーションがメインスレッド外で実行されることを意味し、JavaScriptが何をしていてもスムーズさを保ちます。「JS」と「メインスレッド」は同義語ではありません: WAAPIに処理を委譲するJSライブラリはアクセラレーション可能ですが、rAFループはできません。
確実にアクセラレーションされる: transformとopacity。サポートが拡大中: filter、clip-path、background-color。
注目すべきパターンは、ページが重い処理を実行している間に動作するモーションです: ページ遷移、ナビゲーション中のタブインジケーター、データフェッチやハイドレーションと同時に発生するものなど。Vercelのダッシュボードは、アクティブなタブインジケーターをFramer Motionの共有レイアウトアニメーションでアニメーション化していました。ブラウザがちょうどその瞬間に次のページをロードしていたため、フレームをドロップしました。CSSアニメーションに移動することで修正されました。
Motionはopacity、transform、clip-path、filterをWAAPIに委譲するため、これらはアクセラレーション可能です。x、y、scale、rotateのショートハンドはバージョンに依存します:
<motion.div animate={{ x: 100 }} /> // motion 14.2以上: ハードウェアアクセラレーション。それ以前: メインスレッド、カクつく可能性あり
<motion.div animate={{ transform: "translateX(100px)" }} /> // すべてのバージョンでハードウェアアクセラレーション
motion 14.2以降、HTML要素のショートハンドは1つのアクセラレーションされたtransformアニメーションに結合されるため、そのまま使用してください。古いバージョンでは、アニメーションがビジーなメインスレッドを生き残らなければならない場合、文字列形式に切り替えてください。すべてのバージョンにおいて、これらはメインスレッドに留まります: レイアウトアニメーション(layout、layoutId)、onUpdate、transformTemplate、慣性、およびフレームごとにmotion value(ドラッグ、useTransform)によって駆動される値。
アクセラレーションはプログレッシブエンハンスメントです。 ブラウザはバージョンによって変化する条件下で静かにそれを拒否します。例えば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個以上のアイテムを保持すると遅くなりましたが、再レンダリングは関係ありませんでした。ドラッグ距離は、translateY()が読み取るCSS変数に書き込まれていました:
// すべての子孫のスタイルをフレームごとに再計算
const style = { "--swipe-amount": `${draggedDistance}px` };
// この要素のみに影響
const style = { transform: `translateY(${draggedDistance}px)` };
CSS変数は継承されるため、親で変更するとすべての子のスタイル再計算がトリガーされます。コンテンツが多ければ多いほど、各フレームは遅くなります。transformアニメーションがコンテンツの増加に伴って劣化する場合は、これを疑ってください。
ぼかし。 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変数がそもそもアニメーション化されているか? または、14.2以前のバージョンでのMotionの
x/y/scale? - 20pxを超えるアニメーション化された
blur()があるか? - それでもずれたりスタッターしたりするか? 今度はアニメーション化する要素に
will-change: transformを追加します。
感覚ではなく、録画で確認してください: フレームドロップと長時間タスクについてはDevToolsのパフォーマンスパネル、アニメーションをスローダウンするにはアニメーションパネルを使用します。実際のミッドレンジのスマートフォンでリモートデバッグを介してプロファイルしてください。デスクトップでのCPUスロットリングは、弱いGPUや小さなメモリ予算ではなく、遅いCPUをモデル化します。
フェーズ3: 報告する
各所見について:
**[要素]**: `ファイル.tsx:42`
原因: [上記の原因のどれ]
修正前: `transition: all 200ms`(`padding`をアニメーション化、レイアウトをトリガー)
修正後: `transition: transform 200ms` と `scale(0.97)`
確認方法: コードから / デバイス録画が必要
所見をコスト順に並べてください: レイアウトの前にペイント、ビジーなメインスレッドの前にアイドル。その後、停止します。質問されたら修正を適用します。