60fps-animation について
60fps-animation は、Web 開発者向けのアニメーション性能ガイドです。transform、opacity、FLIP を使ってレイアウトや描画の負荷を抑える方法を解説します。auto 高さのアニメーション、レイアウト読み書きの一括処理、性能検証も扱います。
主な機能
- 01transform と opacity を優先
- 02FLIP によるレイアウト変更のアニメーション
- 03auto 高さのトランジション手法
- 04レイアウト読み書きの一括処理
- 05アニメーションの停止・撮影・性能検証
- 06will-change の慎重な使用
活用シーン
- 01アニメーションのカクつきやフレーム落ちの調査
- 02移動・並べ替え・サイズ変更の滑らかな表現
- 03展開パネルやホバー時の影の実装
- 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を呼び出します。
- CSS の
検証手順 — 停止 → スクリーンショット撮影。その後、ジャンクをプロファイル:
- 開始時/中間/終了時(
?t=0、中間、終了)にヘッドレスで開き、それぞれスクリーンショットを撮影します。動きが視覚的に正しいことを確認します(scaleXによるテキストのクリッピングや伸縮がないこと、FLIP が実際のレイアウト位置に到達すること、影/高さのトランジションが正しく見えること)。 - コンポジターレイヤーのみであることを確認します — これがこのスキルの要点です。次のいずれかを実行します。
- DevTools → Performance:アニメーションを記録し、フレームごとに紫色の「Layout」または緑色の「Paint」帯がなく、「Composite Layers」のみであることを確認します。
- またはヘッドレストレース:見た目の確認には
npx playwright screenshotを使用し、CDP/tracingキャプチャを追加して、アニメーション区間中にLayout/Paintイベントが発生しないことを確認します。
- FPS/「Frame Rendering Stats」オーバーレイが 60 のままか確認します。フレーム落ちがある場合は、コストの高いプロパティが再び混入しています。
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=0.3" frame.png
完了前の確認事項:
- スタンドアロンで開けること — コンソールエラーがないこと。
- フレームごとにアニメーションするのは
transform/opacityのみであること。DevTools にフレームごとの Layout/Paint が表示されないこと。 - 開始時/中間/終了時のスクリーンショットを撮影済みであること — 見た目が正しく、
scaleXによるテキストの歪みがなく、FLIP が正しい位置に到達すること。 - 60fps を維持すること。
will-changeを必要な直前に適用し、アイドル状態では解除すること(レイヤーを残さない)。 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 のプロファイリング手順、視差効果を減らすパターン、プロパティのコスト一覧。