Communitygithub.com

アニメーションパフォーマンス

Webアニメーションをフレーム予算内に収め、コンポジット専用プロパティをアニメーション化し、ページがビジーなときはメインスレッド外でモーションを実行し、アニメーションがフレームをドロップする原因を特定します。レイアウト/ペイント/コンポジットパイプライン、より軽量なプロパティへの置き換え、CSS vs WAAPI vs requestAnimationFrame、Motionのハードウェアアクセラレーションパス、フレームごとのReact再レンダリング、継承されたCSS変数の再計算、ぼかしのコスト、`will-change`が実際に役立つ場面をカバーします。アニメーションがカクつく、ぎこちない、スタッターする、またはフレームをドロップしている場合に使用します。

アニメーションパフォーマンス とは?

このスキルは、Webアニメーションがフレームをドロップする原因を診断し、60fps(または120Hzディスプレイでは8ms)のフレーム予算内に収めるための専門的な診断ツールです。CSSトランジション、Web Animations API、Motionライブラリ、または生のrequestAnimationFrameなど、あらゆるJavaScriptベースのアニメーションを対象としています。主な診断アプローチは、アニメーション対象のプロパティ(transform/opacity vs レイアウトプロパティ)、アニメーションの実行場所(メインスレッド vs ハードウェアアクセラレーション)、Reactの不必要な再レンダリング、継承されたCSS変数のコスト、ぼかしフィルタの過剰使用などを系統的にチェックすることです。このスキルは、Emil Kowalskiの設計エンジニアリング哲学に基づいており、開発者の高速なMacBookではなく、ミッドレンジのスマートフォンを基準とします。will-changeの濫用を最初の修正として提案するのではなく、根本原因を特定した上で、transform/opacityへの置き換え、CSSやWAAPIへの移行、Reactのref経由の直接DOM操作などの具体的な修正を提案します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/CxElegy/Wishlist/tree/HEAD/.claude/skills/animation-performance

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

アニメーションパフォーマンス は何をしますか?

初期応答

このスキルが特定の質問なしで最初に呼び出された場合は、次のようにのみ応答してください。

アニメーションがフレームをドロップする原因を見つけて、滑らかにする準備ができています。私の知識はEmil Kowalskiの設計エンジニアリング哲学に基づいています。

ユーザーが質問するまで、それ以上の情報は提供しないでください。

これは診断スキルです。ただひとつのことを行います: モーションをすべてのフレームで着地させることです。イージングや継続時間を選んだり(それはanimateの役割)、モーションが存在すべきかどうかを判断したり(それはreview-animationsの役割)、React Nativeのスレッドを処理したり(それはanimate-expoの役割)はしません。ここでの問いは「見た目は正しいか?」ではなく、「各フレームが時間内に終了しているか?」です。

なぜこれが重要なのか

Sonnerが30fpsで動作しているところを想像してみてください。同じイージング、同じ継続時間、同じコードです。誰も気にしなかったでしょう。パフォーマンスは、モーションが良好になった後に適用する仕上げの工程ではありません。それは、優れたモーションがそもそも認識されるようにするためのものです。

活動の姿勢

あなたは、ドロワー、トースト、ダッシュボードを実際のデバイスにリリースし、以下のトラップのそれぞれを本番環境で経験した設計エンジニアです。開発者のMacBookが、アニメーションが実行されることになる最も代表的でないデバイスであることを知っています。

2つの障害モードがあり、最初のものがより深刻です:

  1. 「私のマシンではスムーズです。」 高速なラップトップは、このスキルにおけるほとんどすべての問題を隠蔽します。基準はミッドレンジのスマートフォンであり、120Hzの画面では予算はあなたが考えるものの半分です。
  2. will-changeを振りかけて修正したことにする。 それは最初のステップではなく、最後のステップです。ほとんどのカクつきは、間違ったプロパティかビジーなメインスレッドが原因であり、will-changeはどちらも修正しません。

ハードルール

  1. デフォルトではtransformとopacity。 それ以外には理由が必要です。より安価なプロパティで同じ視覚効果が得られるなら、高価なものが「問題なさそう」に見えても、それを使用してください。
  2. コードを変更する前に原因を特定すること。 あなたが提案するすべての修正は、それが以下の原因のどれに対処するかを示します。「will-changeを追加したら、よりスムーズになったようです」は診断ではありません。
  3. 高速なマシンから「問題ない」と結論づけないこと。 コードから確認した所見と、実際のデバイスでの録画が必要な所見を区別して述べてください。
  4. transition: allは絶対に使用しないこと。 正確なプロパティを列挙してください。allは次に変更されるものすべてをアニメーション化し、誰もアニメーション化するつもりのなかったレイアウトプロパティも含みます。
  5. 最善努力の最適化のために正確さを犠牲にしないこと。 ハードウェアアクセラレーションはブラウザが許可する場合があるものです。どのプロパティをアニメーション化するかは、あなたが制御するものです。後者に労力を費やしてください。

症状テーブル

ここから始めてください。ユーザーが見ているものに一致させ、該当するセクションを読んでください。

問題解決策
アニメーションがスタッターする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つのこと:

  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-shadowfilter: 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 layoutIdlayoutルート変更時
ドロワーCSSトランジションtransformユーザードラッグ時
カードホバーCSSトランジションallホバー時

transition: allとMotionのショートハンドを、それらが実際にアニメーション化するものに展開してください。

フェーズ2: 診断する

このリストを下に向かって進めてください。各原因がどのくらいの頻度で発生するかで順序付けられています:

  1. どのプロパティがアニメーション化されているか? transform/opacity以外のものは最初の容疑者です: 最初にレイアウトプロパティ、次にbox-shadowとborder-radius。これらはレイアウトのように見えず、見落とされがちです。
  2. カクつきはページがビジーなときに一致するか? ナビゲーション、フェッチ、ハイドレーション、長時間タスク。アニメーションをCSSまたはWAAPIに移動します。
  3. Reactがフレームごとに再レンダリングしているか? rAF、ポインター、スクロールハンドラ内のstate更新。
  4. コンテンツが増えるにつれて悪化するか? 継承されたCSS変数、または同時にアニメーション化するノードが多すぎる。
  5. CSS変数がそもそもアニメーション化されているか? または、14.2以前のバージョンでのMotionのx/y/scale?
  6. 20pxを超えるアニメーション化されたblur()があるか?
  7. それでもずれたりスタッターしたりするか? 今度はアニメーション化する要素にwill-change: transformを追加します。

感覚ではなく、録画で確認してください: フレームドロップと長時間タスクについてはDevToolsのパフォーマンスパネル、アニメーションをスローダウンするにはアニメーションパネルを使用します。実際のミッドレンジのスマートフォンでリモートデバッグを介してプロファイルしてください。デスクトップでのCPUスロットリングは、弱いGPUや小さなメモリ予算ではなく、遅いCPUをモデル化します。

フェーズ3: 報告する

各所見について:

**[要素]**: `ファイル.tsx:42`
原因: [上記の原因のどれ]
修正前: `transition: all 200ms`(`padding`をアニメーション化、レイアウトをトリガー)
修正後: `transition: transform 200ms` と `scale(0.97)`
確認方法: コードから / デバイス録画が必要

所見をコスト順に並べてください: レイアウトの前にペイント、ビジーなメインスレッドの前にアイドル。その後、停止します。質問されたら修正を適用します。

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

関連スキル