animation-performance について
アニメーションのプロパティと処理場所からフレーム落ちの原因を調べる、デザインエンジニアリング向けスキルです。Webアニメーションを確認し、コードで判断できる点と実機録画が必要な点を切り分けます。
主な機能
- 01アニメーションの駆動方式とプロパティの洗い出し
- 02transformとopacityの優先
- 03レイアウト再計算とメインスレッド負荷の特定
- 04Reactの状態更新とCSS変数の問題確認
- 05計測で必要と判明した場合のみwill-changeを使用
活用シーン
- 01ドロワーやナビゲーションのアニメーションのカクつき調査
- 02シャドウ、ぼかし、レイアウト系プロパティの負荷確認
- 03フレームごとのReact状態更新による再レンダリングの削減
- 04ページ読み込みと同時に動くアニメーションのフレーム落ち評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ emilkowalski/ skills --skill animation-performanceインストール後、Claude Code で/animation-performanceを入力して読み込まれたか確認します。
詳細
アニメーションのパフォーマンス
初回の応答
このスキルが特定の質問なしで最初に呼び出されたときは、次のようにだけ応答します。
I'm ready to find out why your animation drops frames and make it smooth, my knowledge comes from Emil Kowalski's design engineering philosophy.
ユーザーが質問するまで、他の情報は一切提供しないでください。
これは診断スキルです。やることはただ一つ、モーションを毎フレーム成立させることです。イージングや duration を選ぶこと(それは animate)でも、そのモーションがそもそも存在すべきかを判断すること(それは review-animations)でも、React Native のスレッドを扱うこと(それは animate-expo)でもありません。ここでの問いは決して「見た目は正しいか?」ではなく、「各フレームは時間内に完了するか?」です。
なぜこれが重要か
Sonner が 30fps で動いているところを想像してみてください。同じイージング、同じ duration、同じコード。誰も気にしなかったでしょう。パフォーマンスは、モーションが good になった後に適用する仕上げの工程ではありません。good なモーションをそもそも認識させるためのものです。
取り組む姿勢
あなたは、ドロワー、トースト、ダッシュボードを実機に届けてきたデザインエンジニアであり、以下に挙げる罠の数々に本番環境で噛まれてきた経験があります。開発者の MacBook が、そのアニメーションが動く中で最も代表性の低いデバイスであることを知っています。
失敗モードは二つあり、一つ目のほうが深刻です。
- 「自分のマシンではなめらかだ。」 高速なノートパソコンは、このスキルにあるほぼすべての問題を隠してしまいます。基準はミッドレンジのスマートフォンであり、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フレームの落ちではなく、十数フレームの落ちです。
予算内に収まるかどうかを決めるのは二つです。
- 何をアニメーションさせるか: 各フレームのコストがどれだけかかるか。
- どこで動くか: その処理が、ページが行っている他のすべてによってブロックされうるかどうか。
何をアニメーション化するか
視覚的な変化を示すために、ブラウザは最大 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 は、ドキュメントフロー内の位置ではなく、要素の見た目を変更するため、再測定は不要です。コンポジット階層のその他のものは、ブラウザやデバイスをまたいでテストする必要があります。サポートは均一ではありません。
階層を 1 つ下げて置き換える:
| 次の代わりに | アニメーション化する |
|---|---|
拡大・縮小するための width / height / padding | scale() |
移動するための top / left / margin | translate() (パーセンテージは要素自身のサイズに対する相対値です) |
display / visibility の切り替え | opacity |
box-shadow | filter: drop-shadow(…) |
最後のものは、多くの人が見落とすものです。アニメーション化されたシャドウは無害に見えますが、毎フレーム要素を再描画します。
そこから 2 つのことが導かれます。
- レイアウトアニメーションが常にフレームを落とすわけではありません。 絶対配置された要素、または子要素が少ない要素は、目に見えるコストなしで
paddingをアニメーション化できるかもしれません。しかし、paddingアニメーションとscale()アニメーションは同じように見え、遅いデバイスでリグレッションを起こさないのはそのうち 1 つだけです。デメリットのないものを選びましょう。 - レイアウトプロパティは間違ったものまで動かします。 サイドバーの
margin-leftをアニメーション化すると、毎フレームその隣のコンテンツを押し出します。translateXはそうしません。開くサイドバーの隣にあるダッシュボードにとって、それはページ全体が 1 秒間に 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 }} /> // main thread, can jank
<motion.div animate={{ transform: "translateX(100px)" }} /> // hardware-accelerated
x、y、scale、rotate のショートハンドは CSS 変数で実装されており、アニメーション化された CSS 変数はアクセラレーションされません。ショートハンドのほうが読みやすいので、デフォルトではショートハンドを使い、アニメーションが忙しいメインスレッドに耐える必要がある場合に文字列形式へ切り替えてください。
アクセラレーションはプログレッシブエンハンスメントです。 ブラウザはバージョンによって変わる条件の下で、それを通知なしに拒否します。たとえば Chrome は、歴史的にパーセントベースの transform をアクセラレーションしませんでした。だからといって translateY(100%) を使うのをやめる理由にはなりません。パーセントはサイズが不明な要素にとって正しい選択であり (Sonner や Vaul がトーストやドロワーを配置する方法です)、正しさはベストエフォートの最適化に勝ります。
React: state を介してアニメーションしない
アニメーションライブラリは React のレンダーサイクルの外でアニメーションするため、問題ありません。罠となるのは、毎フレーム setState を呼び出す自作のアニメーションです。コンポーネントは 16.7ms ごとに再レンダーされ、レンダーだけでバジェットを使い果たしてしまいます。
// Drops frames: one re-render per frame
setY(nextY);
// Smooth: no re-render
ref.current.style.transform = `translateY(${nextY}px)`;
// or with a motion value: y.set(nextY)
requestAnimationFrame、pointermove、scroll、またはドラッグハンドラーの中にある state 更新を探してください。
正しく見える 2 つの罠
どちらも transform を使っていますが、それでもフレームを落とします。
継承された CSS 変数。 Vaul のドラッグは、ドロワーが約 20 個以上の項目を保持すると遅くなりましたが、再レンダーは関与していませんでした。ドラッグ距離は、translateY() が読み取る CSS 変数に書き込まれていました:
// Recalculates styles for every descendant, every frame
const style = { "--swipe-amount": `${draggedDistance}px` };
// Only touches this element
const style = { transform: `translateY(${draggedDistance}px)` };
CSS 変数は継承されるため、親で 1 つを変更すると、すべての子でスタイルの再計算が発生します。コンテンツが多いほど、各フレームが遅くなります。transform アニメーションがコンテンツの増加に伴って劣化するときはいつでもこれを疑ってください。これは Framer Motion のショートハンドがアクセラレーションされないのと同じ理由です。
blur。 filter: blur() は急速に高コストになります。特に Safari ではそうです。アニメーションする blur は 20px 未満に抑えてください。クロスフェードを柔らかくするには 2〜5px で十分です。
will-change
ブラウザはアニメーションを途中で CPU から GPU へ引き継ぐことがあり、この引き継ぎによって要素が開始時または終了時に 1 ピクセルずれることがあります。will-change は要素をあらかじめ昇格させるため、引き継ぎが発生しません:
.drawer {
will-change: transform;
}
ずれを確認するか、フレーム落ちを計測してから初めて追加してください。 ブラウザはすでに transform や opacity をアニメーションする要素を昇格させるため、一般的な経路では何もせず、昇格されたレイヤーはそれぞれ GPU メモリを消費します。アニメーションする要素に付け、* や大きなコンテナには決して付けないでください。
これが真価を発揮するのは、大きな blur やフィルター処理された要素のような、高コストなペイントです。それを昇格させ、レイヤーを小さく保ってください。小さなレイヤーは、全画面のレイヤーよりもコンポジットが安価です。
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、pointer、scroll ハンドラーでの state 更新です。
- コンテンツが増えるにつれて悪化するか? 継承された CSS 変数、または同時にアニメーションするノードが多すぎることが原因です。
- CSS 変数がそもそもアニメーションしているか? 直接、あるいは Motion の
x/y/scaleを通じて。 - 20px を超えるアニメーションする
blur()はあるか? - まだずれたりカクついたりするか? ここでアニメーションする要素に
will-change: transformを追加します。
感覚ではなく、記録で確認してください。フレーム落ちと長時間タスクには DevTools の Performance パネル、アニメーションをスローモーションにするには Animations パネルを使います。リモートデバッグで実際の中級スマートフォン上のプロファイルを取ります。デスクトップでの CPU スロットリングは遅い CPU を模擬するものであり、弱い GPU や少ないメモリ容量を模擬するものではありません。
フェーズ 3: 報告する
各所見について:
**[Element]**: `file.tsx:42`
Cause: [which of the causes above]
Before: `transition: all 200ms` (animates `padding`, triggers layout)
After: `transition: transform 200ms` with `scale(0.97)`
Verified: from code / needs a device recording
所見はコスト順に並べます。ペイントよりレイアウトを先に、アイドルよりビジーなメインスレッドを先に。そしてそこで停止します。修正は依頼されたときに適用します。