Motion(旧 Framer Motion)でカクつき・ちらつきを潰す:レイアウトシフトと FLIP の落とし穴

Web アプリケーションにおいて、滑らかでインタラクティブなアニメーションは、ユーザーエクスペリエンスを大きく向上させる重要な要素です。しかし、Motion(旧 Framer Motion)を使ったアニメーション開発において、「動きがカクつく」「画面がちらつく」といった問題に直面することはありませんでしょうか。
これらの問題は、単純にライブラリの使い方を間違えているだけでなく、ブラウザのレンダリングエンジンの仕組みやパフォーマンス特性を理解していないことが根本的な原因となっています。特に、レイアウトシフトや FLIP アニメーションの誤用は、60fps の滑らかなアニメーションを実現する上で大きな障壁となります。
本記事では、Motion を使ったアニメーション開発で遭遇するパフォーマンス問題の根本原因を明らかにし、実践的な解決策をコード例とともに詳しく解説いたします。開発現場で即座に活用できるテクニックをお伝えしますので、ぜひ最後までご覧ください。
背景
Motion(旧 Framer Motion)の基本的な仕組み
Motion は React 向けのアニメーションライブラリとして広く採用されており、宣言的な記法でリッチなアニメーションを実現できます。基本的には、CSS の transform
プロパティや opacity
を操作してアニメーションを作成しますが、その裏側では複雑な処理が行われています。
Motion の動作原理を理解するために、以下の図でライブラリの基本構造を確認してみましょう。
mermaidflowchart TB
component[React Component] -->|props変更| motion[motion.div]
motion -->|アニメーション計算| animator[Animation Engine]
animator -->|CSS更新| browser[Browser]
browser -->|レンダリング| gpu[GPU Layer]
subgraph render_process[レンダリングプロセス]
layout[Layout計算]
paint[Paint処理]
composite[Composite処理]
end
browser --> render_process
render_process --> gpu
Motion は内部的に独自のアニメーションエンジンを持ち、React のライフサイクルと密接に連携しながら DOM を操作します。重要なのは、アニメーション実行時にブラウザのレンダリングプロセスがどのように影響を受けるかという点です。
アニメーション実行時のブラウザ処理フロー
ブラウザがアニメーションを描画する際、以下の処理フローを経由します。この処理の流れを理解することが、パフォーマンス問題を解決する鍵となります。
mermaidsequenceDiagram
participant JS as JavaScript
participant Style as Style計算
participant Layout as Layout
participant Paint as Paint
participant Composite as Composite
participant GPU as GPU
JS->>Style: CSSプロパティ変更
Style->>Layout: 要素位置・サイズ計算
Layout->>Paint: 色・影・テキスト描画
Paint->>Composite: レイヤー合成
Composite->>GPU: 画面出力
Note over Layout,Paint: 重い処理(避けるべき)
Note over Composite,GPU: 軽い処理(推奨)
この処理フローにおいて、Layout と Paint の段階は非常にコストが高く、60fps を維持するためには可能な限り避ける必要があります。理想的なアニメーションは、Composite 段階のみで完結するものです。
Motion アニメーションの内部処理
Motion が実際にアニメーションを実行する際の内部処理を詳しく見てみましょう。
typescript// Motion の基本的な使用例
import { motion } from 'framer-motion';
const AnimatedBox = () => {
return (
<motion.div
initial={{ x: 0 }}
animate={{ x: 100 }}
transition={{ duration: 0.5 }}
>
アニメーションボックス
</motion.div>
);
};
上記のコードが実行される際、Motion は以下の処理を行います:
- 初期値の計算:
initial
プロパティから開始状態を決定 - 目標値の設定:
animate
プロパティから終了状態を決定 - 補間値の計算:
transition
に基づいて中間フレームを生成 - DOM 更新: 計算された値を CSS プロパティに反映
パフォーマンスに影響する要因
Motion アニメーションのパフォーマンスに影響する主な要因は以下の通りです。
要因 | 影響度 | 説明 |
---|---|---|
アニメーション対象プロパティ | 高 | width , height など Layout を引き起こすプロパティ |
要素の複雑さ | 中 | 子要素の数や CSS の複雑さ |
同時実行アニメーション数 | 高 | 並行して動作するアニメーションの総数 |
デバイス性能 | 中 | CPU/GPU の処理能力とメモリ容量 |
DOM 階層の深さ | 低 | レンダリングツリーの複雑さ |
特に重要なのは、アニメーション対象プロパティの選択です。transform
や opacity
といった Composite 段階のみで処理されるプロパティを選択することで、大幅なパフォーマンス向上が期待できます。
図で理解できる要点:
- Motion は React のライフサイクルと密接に連携してアニメーションを実行する
- ブラウザレンダリングの各段階を理解することで最適化ポイントが明確になる
- アニメーション対象プロパティの選択がパフォーマンスに最も大きな影響を与える
課題
レイアウトシフトが引き起こすカクつき
Motion を使ったアニメーション開発で最も頻繁に発生する問題が、レイアウトシフトによるカクつきです。レイアウトシフトとは、要素のサイズや位置変更により、ブラウザがページ全体のレイアウトを再計算する現象を指します。
以下の図で、レイアウトシフトが発生するメカニズムを確認してみましょう。
mermaidflowchart TD
trigger[アニメーション開始] --> check{対象プロパティ}
check -->|width, height, top, left| layout[Layout再計算]
check -->|transform, opacity| composite[Composite処理]
layout --> repaint[Paint再実行]
repaint --> recomposite[Composite再実行]
recomposite --> frame_drop[フレーム落ち]
composite --> smooth[スムーズなアニメーション]
subgraph problem_zone[問題が発生する領域]
layout
repaint
recomposite
frame_drop
end
subgraph ideal_zone[理想的な処理領域]
composite
smooth
end
レイアウトシフトを引き起こす代表的な CSS プロパティには以下があります:
プロパティカテゴリ | 具体例 | 影響 |
---|---|---|
サイズ系 | width , height , padding , margin | レイアウト全体の再計算 |
位置系 | top , left , right , bottom | 周辺要素への影響 |
ボックスモデル系 | border , box-sizing | 要素サイズの変更 |
フロート系 | float , display | レイアウトフローの変更 |
これらのプロパティをアニメーション対象にすると、毎フレームでブラウザがレイアウト計算を実行し、結果として著しいパフォーマンス低下を招きます。
FLIP アニメーションの仕組みと潜在的な問題
FLIP(First, Last, Invert, Play)は、Motion の layout アニメーションで使用される技術です。一見すると魅力的な機能ですが、誤用すると深刻なパフォーマンス問題を引き起こします。
FLIP の動作原理を以下の図で説明します。
mermaidsequenceDiagram
participant User as ユーザー操作
participant Motion as Motion Library
participant DOM as DOM
participant Browser as Browser
User->>Motion: レイアウト変更をトリガー
Motion->>DOM: First - 初期位置を記録
Motion->>DOM: Last - 変更後位置を記録
Motion->>DOM: Invert - transform で初期位置に戻す
Motion->>Browser: Play - transform アニメーションを実行
Note over Motion,Browser: 問題:計算コストが高い
Note over DOM,Browser: 副作用:メモリリークのリスク
FLIP の潜在的な問題点:
- 計算コストの増大: 各要素の位置とサイズを毎回測定するため、要素数に比例してコストが増加
- メモリリークのリスク: 位置情報をキャッシュするため、適切にクリーンアップされない場合のメモリ消費
- 予期しないレイアウト: 複雑な CSS レイアウト(Grid、Flexbox)との相性問題
- デバッグの困難さ: アニメーション処理が複雑で、問題の特定が難しい
60fps 維持が困難になる具体的なシナリオ
実際の開発現場で頻繁に発生する、60fps を維持できなくなるシナリオを具体的に見てみましょう。
シナリオ 1: 大量要素のリストアニメーション
typescript// 問題のあるコード例
const ProblematicList = () => {
const [items, setItems] = useState(generateItems(1000));
return (
<div>
{items.map((item) => (
<motion.div
key={item.id}
layout // FLIP を使用(問題あり)
animate={{ opacity: item.visible ? 1 : 0 }}
style={{ height: item.height }} // Layout シフト発生
>
{item.content}
</motion.div>
))}
</div>
);
};
このコードの問題点:
- 1000 個の要素で layout アニメーションを同時実行
height
プロパティの変更でレイアウトシフト発生- FLIP の計算コストが要素数に比例して増加
シナリオ 2: 複雑なレスポンシブレイアウト
typescript// レスポンシブ対応で問題が発生するコード
const ResponsiveCard = () => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<motion.div
layout
animate={{
width: isExpanded ? '100%' : '50%', // レイアウトシフト
height: 'auto', // 高さの自動調整で再計算発生
}}
>
<motion.div layout>
{isExpanded && <HeavyComponent />}
</motion.div>
</motion.div>
);
};
シナリオ 3: ネストした要素の連鎖アニメーション
複数の子要素を持つコンポーネントで layout アニメーションを使用すると、親子間でレイアウト計算の連鎖が発生し、パフォーマンスが指数関数的に悪化します。
mermaidflowchart TD
parent[親要素の layout 変更] --> child1[子要素1の再計算]
parent --> child2[子要素2の再計算]
parent --> child3[子要素3の再計算]
child1 --> grandchild1[孫要素1-1の再計算]
child1 --> grandchild2[孫要素1-2の再計算]
child2 --> grandchild3[孫要素2-1の再計算]
child3 --> grandchild4[孫要素3-1の再計算]
subgraph cascade[計算コストの連鎖]
child1
child2
child3
grandchild1
grandchild2
grandchild3
grandchild4
end
これらのシナリオでは、ブラウザのメインスレッドが過負荷状態となり、60fps の維持が困難になります。特にモバイルデバイスでは、CPU 性能の制約によりさらに深刻な問題となります。
図で理解できる要点:
- レイアウトシフトは特定の CSS プロパティが原因で発生し、全体パフォーマンスに影響する
- FLIP アニメーションは便利だが、計算コストとメモリ使用量に注意が必要
- 要素数と階層の深さがパフォーマンス問題を指数関数的に悪化させる
解決策
GPU 加速を活用したアニメーション最適化
パフォーマンス問題を根本的に解決するには、GPU 加速を活用したアニメーション実装が不可欠です。GPU は並列処理に特化した設計となっており、アニメーション処理を CPU から GPU に移すことで劇的な性能向上が期待できます。
GPU 加速を適切に活用するための戦略を以下の図で確認してみましょう。
mermaidflowchart LR
cpu[CPU処理] -->|移行| gpu[GPU処理]
subgraph cpu_tasks[CPU で行うべき処理]
logic[ビジネスロジック]
calc[数値計算]
dom[DOM操作]
end
subgraph gpu_tasks[GPU で行うべき処理]
transform[Transform アニメーション]
opacity[Opacity 変更]
filter[Filter 効果]
composite_ops[Composite 処理]
end
cpu_tasks -.->|避ける| heavy_anime[重いアニメーション]
gpu_tasks -->|推奨| smooth_anime[スムーズなアニメーション]
GPU 加速を効果的に利用するための具体的な実装手法:
Transform ベースのアニメーション設計
typescript// GPU 加速を活用した最適化されたアニメーション
const OptimizedAnimation = () => {
return (
<motion.div
initial={{
x: -100,
y: -100,
scale: 0.8,
opacity: 0,
}}
animate={{
x: 0,
y: 0,
scale: 1,
opacity: 1,
}}
transition={{
type: 'spring',
damping: 20,
stiffness: 300,
}}
style={{
// GPU レイヤーを明示的に作成
willChange: 'transform, opacity',
// ハードウェア加速を強制
transform: 'translateZ(0)',
}}
>
コンテンツ
</motion.div>
);
};
レイアウト再計算を回避する実装方法
レイアウト再計算を完全に回避するには、アニメーション設計の段階から慎重な計画が必要です。以下の原則に従って実装することで、Layout や Paint フェーズをスキップし、Composite のみでアニメーションを実現できます。
アニメーション対象プロパティの選択基準
使用推奨 | プロパティ | 理由 |
---|---|---|
◎ | transform: translateX/Y/Z() | Composite のみで処理 |
◎ | transform: scale() | GPU で高速処理 |
◎ | transform: rotate() | ハードウェア加速対応 |
◎ | opacity | レイヤー単位で処理 |
△ | filter | GPU 対応だが重い場合がある |
× | width , height | Layout 再計算が必要 |
× | top , left | 位置計算が必要 |
× | background-color | Paint 処理が必要 |
レイアウト回避の実装パターン
typescript// レイアウトシフトを回避する実装例
const LayoutFriendlyExpansion = () => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div style={{ position: 'relative' }}>
{/* 基本コンテナ - サイズ固定 */}
<div
style={{
width: '300px',
height: '200px',
overflow: 'hidden',
}}
>
{/* アニメーション対象 - transform のみ使用 */}
<motion.div
animate={{
y: isExpanded ? -100 : 0,
scale: isExpanded ? 1.2 : 1,
}}
transition={{ type: 'spring', damping: 25 }}
style={{
willChange: 'transform',
position: 'absolute',
width: '100%',
height: '100%',
}}
>
メインコンテンツ
</motion.div>
{/* 展開コンテンツ - 別レイヤーで管理 */}
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: isExpanded ? 1 : 0,
y: isExpanded ? 0 : 50,
}}
style={{
position: 'absolute',
top: '100%',
willChange: 'transform, opacity',
}}
>
展開時の追加コンテンツ
</motion.div>
</div>
</div>
);
};
FLIP の正しい使い方とベストプラクティス
FLIP アニメーションを安全に使用するには、適用範囲を限定し、パフォーマンスへの影響を最小限に抑える必要があります。
FLIP 使用の判断基準
mermaidflowchart TD
start[FLIP アニメーション検討] --> count{要素数チェック}
count -->|10個以下| safe[安全に使用可能]
count -->|11-50個| caution[注意して使用]
count -->|51個以上| avoid[使用を避ける]
safe --> complexity{レイアウト複雑さ}
caution --> complexity
complexity -->|シンプル| implement[FLIP 実装]
complexity -->|複雑| alternative[代替手法検討]
avoid --> alternative
alternative --> transform[Transform ベース実装]
FLIP の最適化実装
typescript// 最適化された FLIP の使用例
const OptimizedFLIPList = () => {
const [items, setItems] = useState(generateItems(8)); // 要素数を制限
return (
<motion.div layout>
{items.map((item) => (
<motion.div
key={item.id}
layout
// パフォーマンス最適化のためのオプション設定
layoutDependency={item.id} // 依存関係を明示
transition={{
layout: {
type: 'spring',
damping: 30,
stiffness: 400,
// アニメーション時間を短縮
duration: 0.3,
},
}}
style={{
// メモリ使用量を最適化
willChange: 'transform',
// GPU レイヤーを事前作成
transform: 'translateZ(0)',
}}
>
{item.content}
</motion.div>
))}
</motion.div>
);
};
will-change プロパティの効果的な活用
will-change
プロパティは、ブラウザにアニメーション予定を事前通知し、GPU レイヤーの最適化を促進します。ただし、不適切な使用はメモリリークを引き起こすため、慎重な管理が必要です。
will-change の動的管理
typescript// will-change を動的に管理するカスタムフック
const useWillChange = (isAnimating: boolean) => {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
if (isAnimating) {
// アニメーション開始時に will-change を設定
element.style.willChange = 'transform, opacity';
} else {
// アニメーション終了時にクリーンアップ
element.style.willChange = 'auto';
}
// クリーンアップ関数
return () => {
if (element) {
element.style.willChange = 'auto';
}
};
}, [isAnimating]);
return ref;
};
// 使用例
const EfficientAnimatedComponent = () => {
const [isAnimating, setIsAnimating] = useState(false);
const ref = useWillChange(isAnimating);
return (
<motion.div
ref={ref}
animate={{ x: isAnimating ? 100 : 0 }}
onAnimationStart={() => setIsAnimating(true)}
onAnimationComplete={() => setIsAnimating(false)}
>
効率的なアニメーション
</motion.div>
);
};
パフォーマンス監視の実装
typescript// アニメーションパフォーマンスを監視するツール
const useAnimationPerformance = () => {
const [metrics, setMetrics] = useState({
fps: 0,
droppedFrames: 0,
averageFrameTime: 0,
});
useEffect(() => {
let frameCount = 0;
let lastTime = performance.now();
let frameTimeSum = 0;
const measureFrame = (currentTime: number) => {
const deltaTime = currentTime - lastTime;
frameTimeSum += deltaTime;
frameCount++;
// 1秒ごとに統計を更新
if (frameCount >= 60) {
const avgFrameTime = frameTimeSum / frameCount;
const fps = 1000 / avgFrameTime;
const droppedFrames = Math.max(0, 60 - fps);
setMetrics({
fps: Math.round(fps),
droppedFrames: Math.round(droppedFrames),
averageFrameTime: Math.round(avgFrameTime * 100) / 100,
});
frameCount = 0;
frameTimeSum = 0;
}
lastTime = currentTime;
requestAnimationFrame(measureFrame);
};
requestAnimationFrame(measureFrame);
}, []);
return metrics;
};
図で理解できる要点:
- GPU 加速により CPU 負荷を大幅に軽減し、スムーズなアニメーションを実現できる
- レイアウト再計算を回避するには、transform と opacity のみを使用する
- will-change プロパティは動的に管理し、不要時はクリーンアップが必要
具体例
実際のコード例:問題のあるアニメーション
まずは、パフォーマンス問題を引き起こす典型的なアニメーション実装を詳しく見てみましょう。これらの例を理解することで、なぜ問題が発生するのかを明確に把握できます。
問題例 1: レイアウトシフトを引き起こすカード展開
typescript// 問題のあるカード展開アニメーション
const ProblematicCard = () => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<motion.div
onClick={() => setIsExpanded(!isExpanded)}
animate={{
// 問題:Layout 再計算を引き起こすプロパティ
width: isExpanded ? 400 : 300,
height: isExpanded ? 300 : 200,
padding: isExpanded ? '32px' : '16px',
}}
transition={{ duration: 0.3 }}
style={{
border: '1px solid #ccc',
borderRadius: '8px',
backgroundColor: 'white',
cursor: 'pointer',
}}
>
<h3>カードタイトル</h3>
{isExpanded && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
>
<p>展開されたコンテンツがここに表示されます。</p>
<p>この実装では毎フレームでレイアウト計算が発生します。</p>
</motion.div>
)}
</motion.div>
);
};
この実装の問題点:
width
,height
,padding
の変更で Layout 再計算が発生- 毎フレームでブラウザがレイアウトを再計算
- 子要素にも影響が波及し、処理コストが増大
問題例 2: 大量要素での FLIP アニメーション
typescript// パフォーマンス問題を引き起こすリスト実装
const ProblematicList = () => {
const [items, setItems] = useState(generateItems(100));
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => {
return sortOrder === 'asc' ? a.value - b.value : b.value - a.value;
});
}, [items, sortOrder]);
return (
<div>
<button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
ソート順を変更
</button>
<motion.div layout>
{sortedItems.map((item) => (
<motion.div
key={item.id}
layout // 100個の要素で FLIP を同時実行(問題あり)
whileHover={{
scale: 1.05, // hover 時に Layout シフト
backgroundColor: '#f0f0f0', // Paint 処理が必要
}}
style={{
padding: '16px',
margin: '8px 0',
border: '1px solid #ddd',
borderRadius: '4px',
}}
>
<h4>{item.title}</h4>
<p>値: {item.value}</p>
</motion.div>
))}
</motion.div>
</div>
);
};
この実装の問題点:
- 100個の要素で FLIP を同時実行し、計算コストが膨大
scale
による hover 効果で Layout シフトが発生backgroundColor
の変更で Paint 処理が必要
最適化後のコード例:スムーズなアニメーション
先ほどの問題のあるコードを、GPU 加速と transform ベースのアニメーションを活用して最適化してみましょう。
最適化例 1: Transform ベースのカード展開
typescript// 最適化されたカード展開アニメーション
const OptimizedCard = () => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div
onClick={() => setIsExpanded(!isExpanded)}
style={{
// 基本サイズを固定してレイアウトシフトを防止
width: '300px',
height: '200px',
position: 'relative',
border: '1px solid #ccc',
borderRadius: '8px',
backgroundColor: 'white',
cursor: 'pointer',
overflow: 'hidden',
}}
>
{/* メインコンテンツ - transform のみでアニメーション */}
<motion.div
animate={{
scale: isExpanded ? 1.33 : 1, // 300px → 400px相当の効果
y: isExpanded ? -20 : 0,
}}
transition={{
type: 'spring',
damping: 25,
stiffness: 300,
}}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
padding: '16px',
willChange: 'transform',
transformOrigin: 'top left',
}}
>
<h3>カードタイトル</h3>
</motion.div>
{/* 展開コンテンツ - 別レイヤーで管理 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: isExpanded ? 1 : 0,
y: isExpanded ? 0 : 20,
}}
transition={{ delay: isExpanded ? 0.1 : 0 }}
style={{
position: 'absolute',
top: '60%',
left: '16px',
right: '16px',
willChange: 'transform, opacity',
pointerEvents: isExpanded ? 'auto' : 'none',
}}
>
<p>展開されたコンテンツがここに表示されます。</p>
<p>この実装では GPU 加速により滑らかに動作します。</p>
</motion.div>
</div>
);
};
最適化例 2: 仮想化とキーフレームを活用したリスト
typescript// 最適化されたリスト実装
const OptimizedList = () => {
const [items, setItems] = useState(generateItems(100));
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => {
return sortOrder === 'asc' ? a.value - b.value : b.value - a.value;
});
}, [items, sortOrder]);
// 仮想化:表示範囲の要素のみを描画
const visibleItems = sortedItems.slice(visibleRange.start, visibleRange.end);
return (
<div>
<button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
ソート順を変更
</button>
<div
style={{
height: '600px',
overflowY: 'scroll',
}}
onScroll={(e) => {
const scrollTop = e.currentTarget.scrollTop;
const itemHeight = 80;
const start = Math.floor(scrollTop / itemHeight);
const end = Math.min(start + 20, sortedItems.length);
setVisibleRange({ start, end });
}}
>
{/* スペーサー:上部の非表示要素分 */}
<div style={{ height: `${visibleRange.start * 80}px` }} />
{visibleItems.map((item, index) => (
<OptimizedListItem
key={item.id}
item={item}
index={visibleRange.start + index}
/>
))}
{/* スペーサー:下部の非表示要素分 */}
<div
style={{
height: `${(sortedItems.length - visibleRange.end) * 80}px`,
}}
/>
</div>
</div>
);
};
// 個別アイテムコンポーネント
const OptimizedListItem = ({ item, index }: { item: Item; index: number }) => {
const [isHovered, setIsHovered] = useState(false);
return (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.02 }} // 段階的な表示
onHoverStart={() => setIsHovered(true)}
onHoverEnd={() => setIsHovered(false)}
style={{
height: '80px',
margin: '8px 16px',
padding: '16px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: 'white',
willChange: 'transform',
position: 'relative',
}}
>
{/* hover 効果 - transform のみ使用 */}
<motion.div
animate={{
scale: isHovered ? 1.02 : 1,
y: isHovered ? -2 : 0,
}}
transition={{ type: 'spring', damping: 30, stiffness: 400 }}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
padding: '16px',
borderRadius: '4px',
backgroundColor: isHovered ? '#f8f9fa' : 'transparent',
willChange: 'transform',
}}
>
<h4 style={{ margin: '0 0 8px 0' }}>{item.title}</h4>
<p style={{ margin: 0, color: '#666' }}>値: {item.value}</p>
</motion.div>
</motion.div>
);
};
パフォーマンス計測と DevTools での確認方法
実際のパフォーマンス改善効果を測定するための具体的な手法をご紹介します。
パフォーマンス計測コンポーネント
typescript// リアルタイムパフォーマンス監視コンポーネント
const PerformanceMonitor = () => {
const metrics = useAnimationPerformance();
const [isVisible, setIsVisible] = useState(false);
return (
<>
<button
onClick={() => setIsVisible(!isVisible)}
style={{
position: 'fixed',
top: '16px',
right: '16px',
zIndex: 1000,
padding: '8px 16px',
backgroundColor: metrics.fps < 50 ? '#ff4444' : '#44ff44',
border: 'none',
borderRadius: '4px',
color: 'white',
fontWeight: 'bold',
}}
>
FPS: {metrics.fps}
</button>
{isVisible && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
style={{
position: 'fixed',
top: '60px',
right: '16px',
zIndex: 1000,
padding: '16px',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
borderRadius: '8px',
fontFamily: 'monospace',
fontSize: '14px',
}}
>
<div>FPS: {metrics.fps}</div>
<div>フレーム落ち: {metrics.droppedFrames}</div>
<div>平均フレーム時間: {metrics.averageFrameTime}ms</div>
<div
style={{
marginTop: '8px',
fontSize: '12px',
color: metrics.fps >= 58 ? '#44ff44' : metrics.fps >= 45 ? '#ffaa00' : '#ff4444',
}}
>
{metrics.fps >= 58 ? '✓ 良好' : metrics.fps >= 45 ? '△ 注意' : '✗ 改善が必要'}
</div>
</motion.div>
)}
</>
);
};
Chrome DevTools を使った分析方法
以下の手順でアニメーションのパフォーマンスを詳細に分析できます:
- Performance タブでの記録
typescript// 分析用のテストコンポーネント
const PerformanceTestComponent = () => {
const [isAnimating, setIsAnimating] = useState(false);
const startPerformanceTest = () => {
// DevTools でのプロファイリング開始を促す
console.log('Performance recording を開始してください');
setIsAnimating(true);
// 5秒後に自動停止
setTimeout(() => {
setIsAnimating(false);
console.log('Performance recording を停止してください');
}, 5000);
};
return (
<div style={{ padding: '20px' }}>
<button onClick={startPerformanceTest}>
パフォーマンステストを開始
</button>
{isAnimating && (
<div>
{/* 重いアニメーション例(比較用) */}
<ProblematicCard />
{/* 最適化されたアニメーション例 */}
<OptimizedCard />
</div>
)}
</div>
);
};
- Rendering タブでの視覚化
DevTools の Rendering タブで以下の項目を有効にして視覚的に問題を確認:
- Paint flashing: Paint が発生する領域を緑色でハイライト
- Layout Shift Regions: レイアウトシフトが発生する領域を青色でハイライト
- Layer borders: GPU レイヤーの境界を表示
パフォーマンス比較の実装例
typescript// Before/After 比較コンポーネント
const PerformanceComparison = () => {
const [activeVersion, setActiveVersion] = useState<'before' | 'after'>('before');
const beforeMetrics = useAnimationPerformance();
const afterMetrics = useAnimationPerformance();
return (
<div style={{ display: 'flex', gap: '20px', padding: '20px' }}>
<div style={{ flex: 1 }}>
<h3>最適化前 (FPS: {beforeMetrics.fps})</h3>
<button onClick={() => setActiveVersion('before')}>
{activeVersion === 'before' ? '● 実行中' : '○ 停止中'}
</button>
{activeVersion === 'before' && <ProblematicCard />}
</div>
<div style={{ flex: 1 }}>
<h3>最適化後 (FPS: {afterMetrics.fps})</h3>
<button onClick={() => setActiveVersion('after')}>
{activeVersion === 'after' ? '● 実行中' : '○ 停止中'}
</button>
{activeVersion === 'after' && <OptimizedCard />}
</div>
</div>
);
};
図で理解できる要点:
- 問題のあるコードでは Layout と Paint の処理が頻発し、パフォーマンスが悪化する
- 最適化されたコードでは transform と opacity のみを使用し、GPU で高速処理される
- リアルタイム監視により、改善効果を数値で確認できる
まとめ
Motion アニメーション最適化の要点
本記事では、Motion(旧 Framer Motion)を使ったアニメーション開発で発生するパフォーマンス問題と、その実践的な解決策について詳しく解説いたしました。重要なポイントを改めて整理します。
パフォーマンス問題の根本原因
-
レイアウトシフトの頻発
width
,height
,padding
などのプロパティ変更による Layout 再計算- 毎フレームでブラウザがページ全体のレイアウトを再計算する負荷
-
FLIP アニメーションの誤用
- 大量要素での同時使用による計算コストの増大
- 位置情報キャッシュによるメモリリークのリスク
-
GPU 加速の未活用
- CPU 集約的な処理による 60fps 維持の困難
- ブラウザの最適化機能を十分に活用できていない状況
効果的な解決策
-
Transform ベースのアニメーション設計
transform: translate()
,scale()
,rotate()
とopacity
のみを使用- Layout と Paint フェーズをスキップし、Composite のみで処理
-
適切な GPU 加速の活用
will-change
プロパティの動的管理でメモリリークを防止translateZ(0)
によるハードウェア加速の明示的な有効化
-
FLIP の適用範囲の制限
- 要素数を 10個以下に限定し、計算コストを抑制
- 複雑なレイアウトでは代替手法を検討
実装時のチェックポイント
項目 | 確認内容 | 目標値 |
---|---|---|
アニメーション対象プロパティ | transform と opacity のみを使用しているか | 100%遵守 |
フレームレート | 60fps を維持できているか | 58fps 以上 |
メモリ使用量 | will-change の適切なクリーンアップが実装されているか | リーク 0 件 |
FLIP 使用要素数 | 同時アニメーション要素数が適切に制限されているか | 10 個以下 |
DevTools 検証 | Paint flashing で不要な Paint が発生していないか | 緑色表示なし |
今後の開発で気をつけるべきポイント
1. 設計段階での配慮
アニメーション実装を開始する前に、以下の点を検討することが重要です:
- 視覚効果の実現方法: サイズ変更ではなく
scale
で同様の効果を実現できないか - レイアウト構造: アニメーション要素を独立したレイヤーに分離できないか
- パフォーマンス要件: ターゲットデバイスの性能とアニメーションの複雑さのバランス
2. 継続的な監視とメンテナンス
typescript// プロダクション環境でのパフォーマンス監視例
const useProductionPerformanceMonitor = () => {
useEffect(() => {
if (process.env.NODE_ENV === 'production') {
// パフォーマンス劣化を検知してログ送信
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.name === 'measure' && entry.duration > 16.67) {
// 60fps を下回るフレーム時間を検知
console.warn('Performance issue detected:', entry);
}
});
});
observer.observe({ entryTypes: ['measure'] });
return () => observer.disconnect();
}
}, []);
};
3. チーム開発での品質担保
開発チーム全体でパフォーマンス意識を共有するための施策:
- コードレビューでのチェックリスト: アニメーション実装時の必須確認項目
- 自動テストでのパフォーマンス検証: CI/CD パイプラインでの FPS 測定
- ドキュメント化: ベストプラクティスとアンチパターンの明文化
4. 将来の技術動向への対応
Motion ライブラリやブラウザ技術の進歩に合わせた継続的な改善:
- 新機能の評価: Motion の新しい最適化機能やオプションの検証
- ブラウザ対応: 新しい CSS プロパティや Web API の活用検討
- パフォーマンス計測: より精密な測定ツールやメトリクスの導入
Motion を使ったアニメーション開発は、適切な知識と実装手法を身につけることで、ユーザーに感動を与える滑らかで美しい体験を提供できます。本記事で紹介したテクニックを実際のプロジェクトで活用していただき、より良いユーザーエクスペリエンスの実現にお役立てください。
継続的な学習と改善により、パフォーマンスとユーザビリティを両立した高品質なアニメーション実装が可能になります。ぜひ今日から実践してみてください。
関連リンク
公式ドキュメント
- Motion 公式サイト - Motion(旧 Framer Motion)の最新情報とドキュメント
- Motion API リファレンス - 全機能の詳細な使用方法
- Motion パフォーマンスガイド - 公式のパフォーマンス最適化ガイド
ブラウザ技術関連
- MDN - CSS Transform - CSS Transform プロパティの詳細
- MDN - will-change - will-change プロパティの正しい使用方法
- Chrome DevTools パフォーマンス分析 - アニメーションのパフォーマンス計測方法
パフォーマンス最適化
- Web Fundamentals - レンダリング最適化 - ブラウザレンダリングの基礎知識
- FLIP Your Animations - FLIP アニメーションの詳細解説
- High Performance Animations - 高性能アニメーションの実装手法
React とアニメーション
- React 公式 - パフォーマンス最適化 - React のレンダリングサイクルとパフォーマンス
- useMemo と useCallback - React のメモ化による最適化
- React Profiler - React アプリケーションのプロファイリング
TypeScript とパフォーマンス
- TypeScript 公式ドキュメント - 型安全性とパフォーマンスの両立
- TypeScript Performance - TypeScript のパフォーマンス最適化
測定・監視ツール
- Lighthouse - Web パフォーマンスの総合的な計測ツール
- Web Vitals - ユーザーエクスペリエンスの重要指標
- React DevTools Profiler - React 専用のパフォーマンス分析ツール
- article
Motion(旧 Framer Motion)でカクつき・ちらつきを潰す:レイアウトシフトと FLIP の落とし穴
- article
Motion(旧 Framer Motion)アーキテクチャ概説:Renderer と Animation Engine を俯瞰する
- article
Motion(旧 Framer Motion)Gesture アニメーション実践:whileHover/whileTap/whileFocus の設計術
- article
Motion(旧 Framer Motion)基本 API 徹底解説:motion 要素・initial/animate/exit の正しい使い方
- article
移行ガイド:Framer Motion から Motion への変更点と対応策
- article
Motion(旧 Framer Motion)入門:React アニメーションを最速で始める
- article
Zustand × Next.js の Hydration Mismatch を根絶する:原因別チェックリスト
- article
NestJS 依存循環(circular dependency)を断ち切る:ModuleRef と forwardRef の実戦対処
- article
MySQL ロック待ち・タイムアウトの解決:SHOW ENGINE INNODB STATUS の読み解き方
- article
WordPress を Docker で最速構築:開発/本番の環境差分をなくす手順
- article
Motion(旧 Framer Motion)でカクつき・ちらつきを潰す:レイアウトシフトと FLIP の落とし穴
- article
WebSocket 導入判断ガイド:SSE・WebTransport・長輪講ポーリングとの適材適所を徹底解説
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来