mcpField
6
60fps-animation

60fps-animation

github.com/iart-aiデザイン・UI632.7k 回インストール18 週間前に更新
リポジトリ

60fps-animation について

60fps-animation は、Web 開発者向けのアニメーション性能ガイドです。transform、opacity、FLIP を使ってレイアウトや描画の負荷を抑える方法を解説します。auto 高さのアニメーション、レイアウト読み書きの一括処理、性能検証も扱います。

主な機能

  1. 01transform と opacity を優先
  2. 02FLIP によるレイアウト変更のアニメーション
  3. 03auto 高さのトランジション手法
  4. 04レイアウト読み書きの一括処理
  5. 05アニメーションの停止・撮影・性能検証
  6. 06will-change の慎重な使用

活用シーン

  1. 01アニメーションのカクつきやフレーム落ちの調査
  2. 02移動・並べ替え・サイズ変更の滑らかな表現
  3. 03展開パネルやホバー時の影の実装
  4. 04アニメーションによるレイアウトや描画の発生確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/iart-ai/web-animation-skills --skill 60fps-animation

インストール後、Claude Code で/60fps-animationを入力して読み込まれたか確認します。

同梱ファイルREADME.mdreferences/patterns-and-profiling.md

詳細

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

カクつく Web アニメーションの最大の原因を取り除きます。毎フレーム、ブラウザーにレイアウトの再計算(リフロー)や再描画を強制するプロパティをアニメーションさせないでください。ブラウザーの描画は、レイアウト → ペイント → 合成の段階で行われます。width、height、top、left、margin、padding、box-shadow、filter をアニメーションさせると、毎フレーム、レイアウトやペイントが再実行され、メインスレッドがブロックされます。transform と opacity のアニメーションはコンポジター(多くの場合 GPU)上だけで実行され、レイアウトとペイントをスキップします。そのため、アニメーションが 60/120fps で滑らかに動作します。

使用する場面

アニメーションがカクついたりフレームが落ちたりするとき、ホバーやスクロールのエフェクトが重く感じられるとき、サイズ、位置、影をアニメーションさせるとき、レイアウトの変化を滑らかにアニメーションさせる必要があるとき(カードの並べ替え、要素のコンテナー間の移動など)、height: auto をアニメーションさせるとき、またはアニメーションコードのパフォーマンスを確認するときに使用します。

基本ルール:アニメーションさせるのは transform と opacity のみ

コストの高いアニメーションはすべて、コストの低い方法に置き換えます。

アニメーション対象(コスト高)発生する処理置き換え先(コスト低)
width / heightレイアウト + ペイントtransform: scaleX()/scaleY()(実際のサイズ変更には FLIP)
top / left / marginレイアウト + ペイントtransform: translate()
box-shadowペイント影を持たせた疑似要素の opacity をアニメーションさせる
filter: blur()ペイント(高負荷)opacity で 2 つのレイヤーをクロスフェードするか、必要最小限に使用する
background-positionペイント子レイヤーに transform: translate() を適用する
color / backgroundペイント多くの場合は許容可能。あるいはレイヤーをクロスフェードする
transform による位置とサイズの変更
/* BAD: animates layout every frame */
.box { transition: left 300ms, width 300ms; left: 0; width: 100px; }
.box:hover { left: 200px; width: 200px; }

/* GOOD: compositor-only */
.box {
  transition: transform 300ms ease;
  transform: translateX(0) scaleX(1);
  transform-origin: left center;
}
.box:hover { transform: translateX(200px) scaleX(2); }

scaleX は内部コンテンツ(テキストなど)を引き伸ばして歪ませます。歪ませずに実際のサイズを変更するには、FLIP を使用します。

疑似要素の opacity による低コストな box-shadow

box-shadow をアニメーションさせると、ぼかしの広い領域が毎フレーム再描画されます。代わりに、::after に影を一度だけ描画し、その opacity だけをアニメーションさせます。

.card { position: relative; }
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 12px 28px rgba(0,0,0,0.35);
  opacity: 0;
  transition: opacity 300ms ease;
  pointer-events: none;
}
.card:hover::after { opacity: 1; }

ぼかした影は一度だけラスタライズされます。ホバー時に変わるのはコンポジター上の opacity だけなので、どのフレームレートでも滑らかに動作します。

FLIP:レイアウトの変更を低コストでアニメーションさせる

FLIP(First、Last、Invert、Play)は、transform だけを使ってレイアウトの変更(並べ替え、サイズ変更、コンテナー間の移動)をアニメーションさせます。要素の変更前の位置(First)と変更後の位置(Last)を測定し、見た目上は動いていないように反転する transform を適用してから、その transform をアニメーションさせて初期状態に戻します(Play)。DOM は実際の最終レイアウトになります。移動処理はすべてコンポジター上で行われます。

function flip(el, mutate) {
  const first = el.getBoundingClientRect();   // First
  mutate();                                    // change the DOM/layout
  const last = el.getBoundingClientRect();     // Last

  const dx = first.left - last.left;
  const dy = first.top - last.top;
  const sx = first.width  / last.width;
  const sy = first.height / last.height;

  el.animate(
    [
      { transformOrigin: 'top left',
        transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})` }, // Invert
      { transformOrigin: 'top left', transform: 'none' },               // Play
    ],
    { duration: 300, easing: 'cubic-bezier(0.2, 0, 0, 1)' }
  );
}

// usage: animate an element moving to a new grid position
flip(card, () => targetContainer.appendChild(card));

Web Animations API の animate() は、コンポジター上で transform を実行します。要素が多数ある場合は、変更を加える前にすべての getBoundingClientRect() の読み取りをまとめて行います(後述のレイアウトスラッシングを参照)。

height: auto のアニメーション

従来、height: auto は補間できませんでした。現在は、次のモダンな方法とフォールバック方法があります。

/* Modern (Chrome 129+/supporting browsers): opt size keywords into animation */
.panel {
  interpolate-size: allow-keywords;   /* enables animating to/from auto */
  height: 0;
  overflow: clip;
  transition: height 300ms ease;
}
.panel.open { height: auto; }
/* calc-size() also works: height: calc-size(auto, size); */
/* Robust fallback today: CSS grid 1fr -> 0fr */
.wrapper {
  display: grid;
  grid-template-rows: 0fr;            /* collapsed */
  transition: grid-template-rows 300ms ease;
}
.wrapper.open { grid-template-rows: 1fr; }
.wrapper > .content { overflow: hidden; min-height: 0; }

グリッドを使う方法はトラックサイズをアニメーションさせます(コンポジターとの相性もよく、幅広くサポートされています)。また、JS による高さの測定も不要です。対象のブラウザーが対応している場合は interpolate-size: allow-keywords / calc-size(auto, size) を使用します。クロスブラウザー対応のデフォルトとしては、グリッドを使う方法を維持してください。

レイアウトスラッシングを避ける(読み取りの後に書き込みをまとめる)

書き込みの後にレイアウトプロパティ(offsetWidth、getBoundingClientRect、scrollTop、getComputedStyle)を読み取ると、同期的なリフローが強制されます。ループ内で読み取りと書き込みを交互に行う「レイアウトスラッシング」は、1 フレームあたり何度もの強制リフローを発生させる可能性があります。

// BAD: read, write, read, write... forces reflow each iteration
items.forEach((el) => {
  const w = el.offsetWidth;          // read (forces layout)
  el.style.width = w * 1.5 + 'px';   // write (invalidates layout)
});

// GOOD: batch all reads, then all writes
const widths = items.map((el) => el.offsetWidth);  // all reads
items.forEach((el, i) => {                          // all writes
  el.style.width = widths[i] * 1.5 + 'px';
});

フレームと同期した処理では、requestAnimationFrame のコールバック内で読み取りを行い、書き込みを適用します。fastdom などのライブラリーは、この読み取りと書き込みのスケジューリングを体系化しています。

will-change:必要な場合にのみ使用する

will-change: transform を指定すると、アニメーション開始時の引っかかりを避けるため、あらかじめ要素が独自のコンポジターレイヤーに昇格します。ただし、昇格したレイヤーはそれぞれ GPU メモリを消費し、使いすぎるとパフォーマンスが低下します。

.menu { will-change: transform; }   /* only on elements about to animate */

ルール:アニメーションの直前(例:ホバー時や親要素の状態変化時)に適用し、アイドル状態になったら解除(will-change: auto)します。多くの要素に一括で適用してはいけません。また、大きなノードや多数のノードに適用したままにしてはいけません。transform: translateZ(0) というハックでも同様に昇格しますが、元に戻しにくいため、will-change を優先してください。

納品と検証(スタンドアロン HTML)

パッケージ化されたヘルパー(scripts/):scripts/seek-shot.sh anim.html 0 1.5 3 は ?t=N のハーネスを使って各時点でアニメーションを停止し、スクリーンショットを撮影します。scripts/contact-sheet.sh sheet.png frame-*.png はそれらをタイル状に並べ、一目で確認できるようにします。詳しくは scripts/README.md を参照してください。

納品物は、ブラウザーで直接開ける自己完結型の .html ひとつです。マークアップ、CSS/JS アニメーション、停止用ハーネスをすべて 1 つのファイルにまとめます。このスキルの検証では、2 つの観点を確認します。フレームの見た目が正しいこと、そして生成コストが低いこと(コンポジターレイヤーのみ)です。

出力要件:

  • .html を 1 つ。依存関係があれば CDN 経由とし、アニメーションは CSS トランジション/@keyframes または Web Animations API で制御します。
  • スクリーンショットを決定論的なフレームで撮影できる停止機構:
    • CSS の @keyframes → ?t=N で el.style.animationDelay = (-N)+'s'; el.style.animationPlayState = 'paused' を設定します。
    • WAAPI/JS → アニメーションオブジェクトを保持し、anim.pause(); anim.currentTime = N*1000 を呼び出します。

検証手順 — 停止 → スクリーンショット撮影。その後、ジャンクをプロファイル:

  1. 開始時/中間/終了時(?t=0、中間、終了)にヘッドレスで開き、それぞれスクリーンショットを撮影します。動きが視覚的に正しいことを確認します(scaleX によるテキストのクリッピングや伸縮がないこと、FLIP が実際のレイアウト位置に到達すること、影/高さのトランジションが正しく見えること)。
  2. コンポジターレイヤーのみであることを確認します — これがこのスキルの要点です。次のいずれかを実行します。
    • DevTools → Performance:アニメーションを記録し、フレームごとに紫色の「Layout」または緑色の「Paint」帯がなく、「Composite Layers」のみであることを確認します。
    • またはヘッドレストレース:見た目の確認には npx playwright screenshot を使用し、CDP/tracing キャプチャを追加して、アニメーション区間中に Layout/Paint イベントが発生しないことを確認します。
  3. FPS/「Frame Rendering Stats」オーバーレイが 60 のままか確認します。フレーム落ちがある場合は、コストの高いプロパティが再び混入しています。
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=0.3" frame.png

完了前の確認事項:

  1. スタンドアロンで開けること — コンソールエラーがないこと。
  2. フレームごとにアニメーションするのは transform/opacity のみであること。DevTools にフレームごとの Layout/Paint が表示されないこと。
  3. 開始時/中間/終了時のスクリーンショットを撮影済みであること — 見た目が正しく、scaleX によるテキストの歪みがなく、FLIP が正しい位置に到達すること。
  4. 60fps を維持すること。will-change を必要な直前に適用し、アイドル状態では解除すること(レイヤーを残さない)。
  5. prefers-reduced-motion に準拠すること。

クイックリファレンス

目的対応方法
要素を移動するtransform: translate()
歪ませずにサイズを変更するtransform を使った FLIP
ホバー時の影影用の疑似要素の opacity をアニメーションさせる
コンテンツの高さまで展開するgrid の 0fr→1fr、または interpolate-size: allow-keywords
多数の要素を移動するgetBoundingClientRect の読み取りをまとめてから、書き込みを行う
アニメーション開始を滑らかにするwill-change を必要な直前に適用し、アイドル状態では解除する
コンポジターレイヤーのみであることを確認するDevTools Performance:フレームごとに紫色の「Layout」/緑色の「Paint」がないこと

注意点

  • scaleX/scaleY はテキストや子要素を伸縮させます。コンテンツの鮮明さを保つ必要がある場合は FLIP を使用してください。
  • transform のパーセンテージは親要素ではなく、要素自身のボックスを基準にします。left: % とは異なります。
  • will-change や translateZ(0) を使いすぎるとレイヤーが増えすぎて、パフォーマンスが低下します。昇格させるのはアニメーションする要素だけにしてください。
  • filter と backdrop-filter はコンポジターに関連しますが、それでもコストが高くなります。ぼかし半径をアニメーションさせるのではなく、透明度のクロスフェードで表示状態をアニメーションさせてください。
  • grid の 1fr→0fr で折りたたむコンテンツには overflow: hidden と min-height: 0 が必要です。そうしないと折りたためません。
  • 必須ではない動きは、必ず @media (prefers-reduced-motion: reduce) の条件下で無効化してください。

リファレンスファイル

  • references/patterns-and-profiling.md — そのまま実行できる完全な例(アコーディオン、並べ替えリスト、パララックス)、コンポジターのみでフレームが描画されることを確認する DevTools のプロファイリング手順、視差効果を減らすパターン、プロパティのコスト一覧。

コメント