animation-performance について
UI アニメーションのフレーム落ちを調べる開発者向け診断スキルです。負荷の高いプロパティや実行経路を追い、原因に基づく修正案を提示します。
主な機能
- 01アニメーションと実行方式を一覧化
- 02負荷の高いアニメーションプロパティを特定
- 03メインスレッドのボトルネックを確認
- 04React の毎フレームの状態更新を診断
- 05CSS 変数とぼかしの負荷を調査
- 06原因を確認してから will-change を使用
活用シーン
- 01ドロワーアニメーションのカクつきを調査
- 02ナビゲーションや読み込み中の遅延を軽減
- 03React のドラッグ操作の遅延を特定
- 04低速なデバイスでの動作を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ playful-design/ router --skill animation-performanceインストール後、Claude Code で/animation-performanceを入力して読み込まれたか確認します。
同梱ファイルLICENSE
詳細
アニメーションのパフォーマンス
初回の応答
このスキルが具体的な質問なしで初めて呼び出された場合は、次の内容だけを返してください。
アニメーションのフレーム落ちの原因を突き止め、滑らかにする準備ができています。Emil Kowalski のデザインエンジニアリング哲学から得た知識を活用します。
ユーザーから質問されるまでは、ほかの情報を提供しないでください。
診断用のスキルです。行うことは一つだけです。すべてのフレームでモーションを確実に描画します。イージングや再生時間は選びません(それは animate の役割です)。モーションを存在させるべきかどうかも判断しません(それは review-animations の役割です)。React Native のスレッドも扱いません(それは animate-expo の役割です)。ここで問うのは、「見た目は正しいか?」ではなく、「各フレームは時間内に処理を完了しているか?」です。
重要な理由
Sonner が 30fps で動作していると想像してください。同じイージング、同じ再生時間、同じコードです。誰も気に留めなかったでしょう。パフォーマンスは、モーションが良くなった後に加える仕上げではありません。良いモーションを認識できるようにするものです。
基本姿勢
実際のデバイス向けにドロワー、トースト、ダッシュボードをリリースし、以下の落とし穴に本番環境で何度も遭遇してきたデザインエンジニアとして対応してください。開発者の MacBook は、アニメーションが実行されるデバイスの中で最も実態を反映しないものだと理解しています。
失敗には二つのパターンがあり、最初のほうが深刻です。
- 「自分のマシンでは滑らかです。」 高速なノートパソコンでは、このスキルで扱う問題のほとんどが隠れてしまいます。基準にするのはミッドレンジのスマートフォンです。120Hz の画面では、使える時間は想定の半分です。
will-changeをあちこちに追加して、直ったことにする。 これは最初ではなく、最後の手順です。カクつきの大半は、プロパティの選択ミスかメインスレッドの負荷が原因で、will-changeではどちらも解決しません。
厳守事項
- デフォルトでは
transformとopacityを使います。 それ以外を使う場合は理由が必要です。より低コストのプロパティで同じ見た目になるなら、高コストのものが「問題なさそう」に見えても、低コストのものを使ってください。 - コードを変更する前に原因を明示します。 提案する修正ごとに、以下の原因のどれに対処するのかを明示してください。「
will-changeを追加したら、滑らかになったようです」は診断ではありません。 - 高速なマシンだけを根拠に「問題ありません」と結論づけないでください。 コードから確認できたことと、実機での録画が必要なことを明示してください。
transition: allは絶対に使わないでください。 対象のプロパティを正確に列挙します。allは次に変更されるプロパティを何でもアニメーションし、意図していないレイアウトプロパティまで対象にします。- 適切な動作を犠牲にして、ベストエフォートの最適化をしないでください。 ハードウェアアクセラレーションはブラウザーが適用するかどうかを決めます。どのプロパティをアニメーションするかは自分で制御できます。後者に注力してください。
症状別一覧
まずここを確認します。ユーザーが目にしている症状と照らし合わせ、該当するセクションを読んでください。
| 問題 | 解決策 |
|---|---|
| アニメーションがカクつく | width/height/top/margin ではなく、transform/opacity をアニメーションする |
| シャドウのアニメーションがぎこちない | filter: drop-shadow() を使う |
| ナビゲーション中または読み込み中だけカクつく | アニメーションを CSS または WAAPI に移し、メインスレッドの外で実行する |
Motion の x/y でフレーム落ちする | motion 14.2 以降にアップグレードするか、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フレームを逃すのではなく、12フレーム分です。
予算内に収められるかどうかを決める要素は2つあります。
- 何をアニメーションさせるか:各フレームの処理にかかる負荷です。
- どこで実行するか:その処理が、ページ上のほかの処理によってブロックされる可能性があるかどうかです。
アニメーションさせるもの
視覚的な変化を表示するために、ブラウザーは最大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()のアニメーションは同じように見えますが、低速なデバイスで性能が悪化しないのは後者だけです。デメリットのないほうを選びましょう。 - レイアウトプロパティは、意図しない要素も動かします。 サイドバーの
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+: hardware-accelerated. Before: main thread, can jank
<motion.div animate={{ transform: "translateX(100px)" }} /> // hardware-accelerated in every version
motion 14.2 以降では、HTML 要素のショートハンドは 1 つのアクセラレーション対応 transform アニメーションにまとめられるため、そのまま使用してください。古いバージョンでは、ビジー状態のメインスレッドでもアニメーションを継続させる必要がある場合、文字列形式に切り替えてください。どのバージョンでも、レイアウトアニメーション(layout、layoutId)、onUpdate、transformTemplate、慣性、モーション値によって毎フレーム駆動される値(ドラッグ、useTransform)はメインスレッドで実行されます。
アクセラレーションは段階的な機能強化です。 ブラウザーは、バージョンによって異なる条件で、何も通知せずにアクセラレーションを行わないことがあります。たとえば Chrome は、歴史的にパーセントベースの transform をアクセラレーションしませんでした。だからといって translateY(100%) の使用をやめる理由にはなりません。サイズが不明な要素にはパーセントが適切です(Sonner と Vaul がトーストやドロワーの位置指定に使っている方法です)。最善努力の最適化より、正しく動作することが優先されます。
React:状態を介してアニメーションさせない
アニメーションライブラリは 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、ドラッグハンドラーの中で状態を更新していないか確認してください。
正しそうに見える 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 変数は継承されるため、親の変数を変更すると、すべての子要素でスタイルの再計算が発生します。コンテンツが増えるほど、各フレームの処理が遅くなります。コンテンツの増加に伴って transform アニメーションが遅くなる場合は、これを疑ってください。
ぼかし。 filter: blur() はすぐに処理負荷が高くなり、特に Safari では顕著です。アニメーションのぼかしは 20px 未満に抑えてください。クロスフェードを柔らかくするには 2~5px で十分です。
will-change
アニメーションの途中で、ブラウザーがアニメーション処理を CPU から GPU に引き継ぐことがあります。その切り替えによって、開始時や終了時に要素が 1 ピクセルずれる場合があります。will-change を指定すると、あらかじめ要素を昇格させ、切り替えを防げます。
.drawer {
will-change: transform;
}
ずれが確認された場合、または計測でフレーム落ちが検出された場合にのみ追加してください。 ブラウザーは通常、transform や opacity をアニメーションする要素をすでに昇格させているため、一般的なケースでは何も変わりません。また、昇格したレイヤーごとに GPU メモリを消費します。指定するのはアニメーションする要素に限り、* や大きなコンテナーには指定しないでください。
効果があるのは、大きなぼかしやフィルターを適用した要素など、ペイント処理にコストがかかる場合です。要素を昇格させ、レイヤーを小さく保ってください。小さなレイヤーの合成は、全画面のレイヤーより低コストです。
JavaScript によるアニメーションは避けるべきですか?
いいえ。メインスレッドでのアニメーションに問題が生じるのは、アニメーション中にページがビジー状態の場合だけです。そのような状況はまれであり、問題の見つけ方もすでに説明しました。
Framer Motion では、CSS では実現できない機能を利用できます。実際のスプリング、レイアウトを共有するアニメーション、アンマウントされるコンポーネントの終了アニメーション、慣性を伴うジェスチャーなどです。問題は JavaScript によるアニメーションを使ってよいかどうかではなく、それらの機能が必要かどうかです。
実用的な使い分け: シンプルな動きやページがビジー状態でも再生される動きには CSS を、複雑で、中断可能な、ジェスチャー駆動の動きには Framer Motion を使います。
ワークフロー
フェーズ 1:アニメーションを特定する
対象となるすべてのアニメーションについて、制御方法とプロパティを一覧にします。
| 要素 | 制御方法 | プロパティ | 再生されるタイミング |
|---|---|---|---|
| タブのインジケーター | motion/react layoutId | レイアウト | ルート変更時 |
| ドロワー | CSS トランジション | transform | ユーザーのドラッグ時 |
| カードのホバー | CSS トランジション | all | ホバー時 |
transition: all と Motion の省略記法は、実際にアニメーションするプロパティに展開して確認します。
フェーズ 2:診断する
次の項目を順に確認します。上にある項目ほど、原因である頻度が高くなっています。
- どのプロパティがアニメーションしていますか?
transform/opacity以外のプロパティがあれば、まず疑ってください。最初にレイアウトプロパティを確認し、次にレイアウト処理とは気づきにくく、見落としやすいbox-shadowとborder-radiusを確認します。 - ページがビジー状態になるタイミングとカクつきは一致しますか? ナビゲーション、データ取得、ハイドレーション、長時間タスクなどが該当します。アニメーションを CSS または WAAPI に移してください。
- フレームごとに React が再レンダリングしていますか? rAF、ポインター、またはスクロールのハンドラー内で状態を更新していないか確認します。
- コンテンツが増えるにつれて悪化しますか? 継承された CSS 変数、または一度にアニメーションするノードが多すぎる可能性があります。
- CSS 変数をアニメーションしていませんか? または、バージョン 14.2 より前の 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
所見はコスト順に並べます。ペイントよりレイアウトを先に、アイドル時よりメインスレッドがビジー状態のときのものを先にします。その後は作業を止めてください。依頼された場合にのみ修正を適用します。