T-CREATOR

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

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 は以下の処理を行います:

  1. 初期値の計算: initial プロパティから開始状態を決定
  2. 目標値の設定: animate プロパティから終了状態を決定
  3. 補間値の計算: transition に基づいて中間フレームを生成
  4. DOM 更新: 計算された値を CSS プロパティに反映

パフォーマンスに影響する要因

Motion アニメーションのパフォーマンスに影響する主な要因は以下の通りです。

要因影響度説明
アニメーション対象プロパティwidth, height など Layout を引き起こすプロパティ
要素の複雑さ子要素の数や CSS の複雑さ
同時実行アニメーション数並行して動作するアニメーションの総数
デバイス性能CPU/GPU の処理能力とメモリ容量
DOM 階層の深さレンダリングツリーの複雑さ

特に重要なのは、アニメーション対象プロパティの選択です。transformopacity といった 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 の潜在的な問題点:

  1. 計算コストの増大: 各要素の位置とサイズを毎回測定するため、要素数に比例してコストが増加
  2. メモリリークのリスク: 位置情報をキャッシュするため、適切にクリーンアップされない場合のメモリ消費
  3. 予期しないレイアウト: 複雑な CSS レイアウト(Grid、Flexbox)との相性問題
  4. デバッグの困難さ: アニメーション処理が複雑で、問題の特定が難しい

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レイヤー単位で処理
filterGPU 対応だが重い場合がある
×width, heightLayout 再計算が必要
×top, left位置計算が必要
×background-colorPaint 処理が必要

レイアウト回避の実装パターン

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, // 300px400px相当の効果
          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 を使った分析方法

以下の手順でアニメーションのパフォーマンスを詳細に分析できます:

  1. 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>
  );
};
  1. 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)を使ったアニメーション開発で発生するパフォーマンス問題と、その実践的な解決策について詳しく解説いたしました。重要なポイントを改めて整理します。

パフォーマンス問題の根本原因

  1. レイアウトシフトの頻発

    • width, height, padding などのプロパティ変更による Layout 再計算
    • 毎フレームでブラウザがページ全体のレイアウトを再計算する負荷
  2. FLIP アニメーションの誤用

    • 大量要素での同時使用による計算コストの増大
    • 位置情報キャッシュによるメモリリークのリスク
  3. GPU 加速の未活用

    • CPU 集約的な処理による 60fps 維持の困難
    • ブラウザの最適化機能を十分に活用できていない状況

効果的な解決策

  1. Transform ベースのアニメーション設計

    • transform: translate(), scale(), rotate()opacity のみを使用
    • Layout と Paint フェーズをスキップし、Composite のみで処理
  2. 適切な GPU 加速の活用

    • will-change プロパティの動的管理でメモリリークを防止
    • translateZ(0) によるハードウェア加速の明示的な有効化
  3. FLIP の適用範囲の制限

    • 要素数を 10個以下に限定し、計算コストを抑制
    • 複雑なレイアウトでは代替手法を検討

実装時のチェックポイント

項目確認内容目標値
アニメーション対象プロパティtransformopacity のみを使用しているか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 を使ったアニメーション開発は、適切な知識と実装手法を身につけることで、ユーザーに感動を与える滑らかで美しい体験を提供できます。本記事で紹介したテクニックを実際のプロジェクトで活用していただき、より良いユーザーエクスペリエンスの実現にお役立てください。

継続的な学習と改善により、パフォーマンスとユーザビリティを両立した高品質なアニメーション実装が可能になります。ぜひ今日から実践してみてください。

関連リンク

公式ドキュメント

ブラウザ技術関連

パフォーマンス最適化

React とアニメーション

TypeScript とパフォーマンス

測定・監視ツール