アニメーションパフォーマンス は何をしますか?
初期応答
このスキルが特定の質問なしで最初に呼び出された場合は、次のようにのみ応答します。
アニメーションがフレームを落とす原因を突き止め、スムーズにする準備はできています。私の知識は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は、脳が滑らかな動きとして認識し、ほとんどのリフレッシュレートに一致する値です。ブラウザに1フレームあたり16.7ms(1000/60)を与えます。120Hz画面(現在ほとんどのスマートフォンと多くのラップトップ)では、8msになります。より小さい方の数値に合わせて設計してください。
規模感として、複雑なページでのレイアウト再計算は100ms以上かかることがあります。それは1フレームの欠落ではなく、10フレーム以上の欠落です。
バジェット内に収まるかどうかは、次の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/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フレームに1回の再レンダリング
setY(nextY);
// スムーズ: 再レンダリングなし
ref.current.style.transform = `translateY(${nextY}px)`;
// またはモーション値を使用: 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ではできないことを提供します。本物のスプリング、共有レイアウトアニメーション、アンマウントされたコンポーネントのexitアニメーション、モメンタムジェスチャーなどです。問うべきは、それらが必要かどうかであり、JSアニメーションが許されるかどうかではありません。
実用的な分割: シンプルなモーションやページがビジーな間に再生されるものにはCSS。複雑で、中断可能で、ジェスチャー駆動のモーションにはFramer Motion。
ワークフロー
フェーズ1: 何がアニメーションしているかを見つける
対象範囲内のすべてのアニメーションを、そのドライバーとプロパティとともにリストアップします。
| 要素 | ドライバー | プロパティ | 再生タイミング |
|---|---|---|---|
| タブインジケーター | motion/react layoutId | layout | ルート変更時 |
| ドロワー | CSS transition | transform | ユーザードラッグ時 |
| カードホバー | CSS transition | 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のパフォーマンスパネル、モーションをスローにするにはアニメーションパネルを使用します。リモートデバッグを介して実際のミッドレンジのスマートフォンでプロファイリングしてください。デスクトップでのCPUスロットリングは、弱いCPUをモデル化しますが、弱いGPUや少ないメモリバジェットはモデル化しません。
フェーズ3: 報告する
各発見事項について:
**[要素]**: `file.tsx:42`
原因: [上記の原因のうちどれか]
修正前: `transition: all 200ms`(`padding`をアニメーションさせ、レイアウトをトリガーする)
修正後: `transition: transform 200ms` と `scale(0.97)`
確認方法: コードから確認済み / デバイス録画が必要
発見事項をコスト順に並べてください。レイアウトはペイントより先に、ビジーなメインスレッドはアイドルより先に。その後は停止します。修正は求められたときに適用してください。