Communitygithub.com

yokyznt/60fps-animation

このスキルは、ユーザーが「CSSアニメーションのカクつきを修正」「アニメーションを60fpsに」「レイアウトスラッシングを止める」「幅/高さ/上/左をスムーズにアニメーション」「アニメーションをtransformに変換」「box-shadowをパフォーマンス良くアニメーション」「高さautoのアニメーション」「FLIPアニメーション」「スクロール/ホバーアニメーションがカクつく理由」を尋ねたときに使用されます。

60fps-animation とは?

このスキルは、Webアニメーションのパフォーマンス問題、特に「カクつき(ジャンク)」の最大の原因である、ブラウザにレイアウト(リフロー)や再描画(ペイント)を強制するプロパティのアニメーションを排除する方法を提供します。ブラウザのレンダリングは「レイアウト → ペイント → コンポジット」の段階を経ますが、`width`や`height`、`top`、`left`、`box-shadow`などのプロパティをアニメーションすると毎フレームレイアウトやペイントが再実行され、メインスレッドをブロックします。一方、`transform`と`opacity`のアニメーションはコンポジター(多くの場合GPU)上で完全に実行され、レイアウトとペイントをスキップするため、60/120fpsで滑らかになります。このスキルは、CSSアニメーションのカクつき修正、アニメーションの60fps化、レイアウトスラッシングの回避、`height: auto`のアニメーション、FLIPテクニック、パフォーマンス検証方法など、実践的なテクニックを網羅しています。対応プラットフォームはClaude Code、Cursor、CodexなどのAIエージェントで、提供されるSKILL.mdに基づいて開発者やAIエージェントが具体的なコード修正と検証手順を理解できます。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/yokyznt/claude-skills-web-definitiva/tree/HEAD/skills/60fps-animation

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

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

ドキュメント

パフォーマンスの高い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()ペイント(高負荷)2つのレイヤーを opacity でクロスフェード、または控えめに使用
background-positionペイント子レイヤーで transform: translate()
color / backgroundペイント多くの場合許容範囲内、またはレイヤーのクロスフェード

transform による位置とサイズ

/* 悪い例:毎フレームレイアウトをアニメーション */
.box { transition: left 300ms, width 300ms; left: 0; width: 100px; }
.box:hover { left: 200px; width: 200px; }

/* 良い例:コンポジターのみ */
.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)を測定し、視覚的に移動していないように見える逆変換を適用し、その変換を元に戻すアニメーション(Play)を実行します。DOMは最終的な実際のレイアウトになります。モーションは純粋なコンポジター処理です。

function flip(el, mutate) {
  const first = el.getBoundingClientRect();   // First
  mutate();                                    // DOM/レイアウトを変更
  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)' }
  );
}

// 使用例:グリッドの新しい位置に移動する要素をアニメーション
flip(card, () => targetContainer.appendChild(card));

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

height: auto のアニメーション

height: auto は歴史的に補間可能ではありませんでした。最新のアプローチとフォールバック:

/* モダン(Chrome 129+/対応ブラウザ):サイズキーワードをアニメーション可能に */
.panel {
  interpolate-size: allow-keywords;   /* auto との間のアニメーションを有効化 */
  height: 0;
  overflow: clip;
  transition: height 300ms ease;
}
.panel.open { height: auto; }
/* calc-size() も使用可能:height: calc-size(auto, size); */
/* 現在の堅牢なフォールバック:CSS grid 1fr -> 0fr */
.wrapper {
  display: grid;
  grid-template-rows: 0fr;            /* 折りたたみ */
  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フレームあたり数十回の強制リフローが発生する可能性があります。

// 悪い例:読み取り、書き込み、読み取り、書き込み… 各イテレーションでリフローが強制される
items.forEach((el) => {
  const w = el.offsetWidth;          // 読み取り(レイアウトを強制)
  el.style.width = w * 1.5 + 'px';   // 書き込み(レイアウトを無効化)
});

// 良い例:すべての読み取りをバッチ処理し、その後すべての書き込み
const widths = items.map((el) => el.offsetWidth);  // すべての読み取り
items.forEach((el, i) => {                          // すべての書き込み
  el.style.width = widths[i] * 1.5 + 'px';
});

フレーム同期処理の場合、requestAnimationFrame コールバック内で読み取りを行い、書き込みを適用します。fastdom のようなライブラリは、この読み取り/書き込みのスケジューリングを形式化します。

will-change:控えめに使用する

will-change: transform は、要素を事前に独自のコンポジターレイヤーに昇格させ、アニメーション開始時のひっかかりを回避します。ただし、昇格したレイヤーはそれぞれGPUメモリを消費し、過剰に使用するとパフォーマンスが低下します。

.menu { will-change: transform; }   /* アニメーションしようとしている要素のみに */

ルール:アニメーションの直前に適用し(例:ホバー時/親の状態)、アイドル時には削除し(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 を参照してください。

納品物は 1つの自己完結型 .html で、ブラウザで直接開くことができ、マークアップ、CSS/JSアニメーション、フリーズハーネスが1つのファイルに含まれています。このスキルでは、検証は2つの側面から行われます。フレームが正しく見えること かつ 生成が低コスト(コンポジターのみ)であることです。

出力契約:

  • 1つの .html、依存関係は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 はコンポジター関連ですが、依然として高負荷です。ブラー半径をアニメーションするのではなく、opacityクロスフェードで存在感をアニメーションします。
  • grid 1fr→0fr のコンテンツには overflow: hidden と min-height: 0 が必要です。そうしないと折りたたまれません。
  • 必須ではないモーションは、常に @media (prefers-reduced-motion: reduce) の後ろにゲートします。

参考ファイル

  • references/patterns-and-profiling.md — 完全な実行可能な例(アコーディオン、リストの並び替え、パララックス)、コンポジターのみのフレームを確認するDevToolsプロファイリングのチュートリアル、動きの軽減パターン、プロパティコストのチートシート。

関連スキル