T-CREATOR

Emotion のパフォーマンス監視運用:Web Vitals× トレースでボトルネック特定

Emotion のパフォーマンス監視運用:Web Vitals× トレースでボトルネック特定

React アプリケーションでスタイリングライブラリの Emotion を利用している開発者の皆さん、こんな悩みはありませんか。「ページの読み込みが遅い」「ユーザー体験が低下している」「どこがボトルネックなのかわからない」――そんな課題を解決するため、本記事では Emotion のパフォーマンス監視運用について、Web Vitals とトレース技術を組み合わせた実践的なアプローチをご紹介します。

初心者の方でも実装できるよう、具体的なコード例と図解を交えながら、段階的に解説していきますね。実際の運用で使えるノウハウが満載ですので、ぜひ最後までお付き合いください。

背景

Emotion とパフォーマンスの関係性

Emotion は CSS-in-JS ライブラリとして、React コンポーネント内でスタイルを記述できる強力なツールです。動的なスタイリングやテーマ機能を簡単に実装できる一方で、実行時にスタイルを生成するという特性から、パフォーマンスへの影響を考慮する必要があります。

特に以下のような場面で、パフォーマンスボトルネックが発生しやすくなるでしょう。

  • コンポーネントの再レンダリングが頻繁に発生する
  • 大量のスタイル定義が動的に生成される
  • ネストが深い複雑なコンポーネント構造

Web Vitals とは何か

Web Vitals は、Google が提唱するユーザー体験を測定するための指標群です。これらの指標は、実際のユーザーがウェブサイトを利用する際の体験を数値化したものとなっています。

主要な 3 つの Core Web Vitals は次のとおりです。

#指標名測定内容目標値
1LCP (Largest Contentful Paint)最大コンテンツの描画時間2.5 秒以内
2FID (First Input Delay)初回入力遅延100 ミリ秒以内
3CLS (Cumulative Layout Shift)累積レイアウトシフト0.1 以内

これらの指標を監視することで、ユーザーが実際に体感するパフォーマンスを把握できます。

トレース技術による可視化の重要性

トレース技術は、アプリケーションの実行プロセスを時系列で記録し、各処理の実行時間や依存関係を可視化する手法です。Emotion のようなランタイムスタイリングライブラリでは、スタイル生成のタイミングや処理時間を詳細に追跡することで、ボトルネックを特定できるようになります。

以下の図は、Emotion を使用したアプリケーションにおける、パフォーマンス監視の全体像を示しています。

mermaidflowchart TB
    user["ユーザー"] -->|"ページ<br/>アクセス"| app["React App<br/>(Emotion)"]
    app -->|"メトリクス<br/>収集"| vitals["Web Vitals<br/>測定"]
    app -->|"トレース<br/>記録"| trace["Performance<br/>Trace"]
    vitals -->|"LCP/FID/CLS"| monitor["監視<br/>ダッシュボード"]
    trace -->|"処理時間<br/>データ"| monitor
    monitor -->|"分析"| bottleneck["ボトルネック<br/>特定"]
    bottleneck -->|"最適化"| app

図で理解できる要点:

  • ユーザーアクセスから監視まで一連のフローが循環する
  • Web Vitals とトレースの両面からデータを収集
  • 継続的な改善サイクルを構築できる

課題

Emotion 特有のパフォーマンス課題

Emotion を使用する際、以下のような課題に直面することがあります。これらは CSS-in-JS ライブラリ共通の課題でもあるため、理解しておくことが重要です。

ランタイムスタイル生成のオーバーヘッド

Emotion は実行時(ランタイム)にスタイルを生成するため、以下のような処理が発生します。

  • スタイルオブジェクトのシリアライゼーション
  • CSS 文字列への変換
  • DOM への style タグの挿入
  • クラス名のハッシュ計算

これらの処理は、コンポーネントがレンダリングされるたびに実行される可能性があり、パフォーマンスに影響を与えます。

動的スタイルによる再計算コスト

props や state に基づいて動的にスタイルを変更する場合、値が変わるたびにスタイルの再計算が必要になるでしょう。

typescript// 動的スタイルの例
const DynamicButton = ({ color, size }) => (
  <button
    css={{
      backgroundColor: color,
      padding: size === 'large' ? '16px 32px' : '8px 16px',
    }}
  >
    Click me
  </button>
);

このようなコンポーネントでは、colorsize が変更されるたびにスタイルが再生成されます。

コンポーネント再レンダリングとスタイル更新の連鎖

React のコンポーネント再レンダリングと Emotion のスタイル更新が連鎖的に発生すると、パフォーマンスが大きく低下する可能性があります。

以下の図は、Emotion におけるパフォーマンス課題の発生フローを示しています。

mermaidflowchart LR
    render["コンポーネント<br/>レンダリング"] -->|"props/state<br/>変更"| calc["スタイル<br/>計算"]
    calc -->|"CSS<br/>生成"| serialize["シリアライズ<br/>処理"]
    serialize -->|"ハッシュ<br/>計算"| hash["クラス名<br/>生成"]
    hash -->|"DOM<br/>操作"| inject["style タグ<br/>挿入"]
    inject -->|"再レンダリング<br/>トリガー"| render

図で理解できる要点:

  • 各処理が連鎖的に実行される構造
  • ループ構造がパフォーマンス劣化を引き起こす
  • 各ステップでの最適化が重要

可視化と特定の難しさ

パフォーマンス問題が発生していることは体感できても、具体的に「どこが」「どの程度」遅いのかを特定するのは容易ではありません。

ブラックボックス化した処理

Emotion 内部の処理は抽象化されているため、外部からは見えづらくなっています。そのため、以下のような疑問が生じるでしょう。

  • スタイル生成に実際どれくらいの時間がかかっているのか
  • どのコンポーネントがボトルネックになっているのか
  • 再レンダリングとスタイル更新の因果関係はどうなっているのか

測定ツールの不足

標準的な開発者ツールだけでは、Emotion 特有のパフォーマンス課題を深掘りするのは困難です。Chrome DevTools の Performance タブである程度は追跡できますが、より詳細な分析には専用の仕組みが必要になります。

本番環境での挙動把握

開発環境では問題なく動作していても、本番環境では異なるパフォーマンス特性を示すことがあります。実際のユーザー環境でのデータ収集が不可欠ですが、適切な監視基盤がなければデータを取得できません。

解決策

Web Vitals による定量的な監視

Web Vitals を導入することで、ユーザー体験を定量的に測定し、パフォーマンスの現状を把握できます。ここでは、実践的な監視アプローチをご紹介しましょう。

web-vitals ライブラリの活用

Google が提供する web-vitals ライブラリを使用すると、簡単に Core Web Vitals を測定できます。

まずはパッケージのインストールから始めます。

bashyarn add web-vitals

次に、測定用の関数を実装していきます。

typescript// src/utils/webVitals.ts
import {
  getCLS,
  getFID,
  getLCP,
  getFCP,
  getTTFB,
} from 'web-vitals';
import type { Metric } from 'web-vitals';

型定義のインポートにより、TypeScript の型安全性を確保しています。

typescript// メトリクスを送信する関数
const sendToAnalytics = (metric: Metric) => {
  // メトリクスデータの構造化
  const body = JSON.stringify({
    name: metric.name, // メトリクス名 (LCP, FID, CLS など)
    value: metric.value, // 測定値
    rating: metric.rating, // 評価 (good, needs-improvement, poor)
    delta: metric.delta, // 前回からの差分
    id: metric.id, // ユニーク ID
    navigationType: metric.navigationType, // ナビゲーションタイプ
  });

  // ビーコン API で送信(ページ遷移時も確実に送信される)
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/analytics', body);
  } else {
    // フォールバック: fetch API を使用
    fetch('/api/analytics', {
      body,
      method: 'POST',
      keepalive: true, // ページ遷移後も送信を継続
    });
  }
};

この関数では、sendBeacon API を優先的に使用することで、ページ遷移時でもデータが確実に送信されるようにしています。

typescript// Web Vitals の測定を開始
export const reportWebVitals = () => {
  getCLS(sendToAnalytics); // Cumulative Layout Shift
  getFID(sendToAnalytics); // First Input Delay
  getLCP(sendToAnalytics); // Largest Contentful Paint
  getFCP(sendToAnalytics); // First Contentful Paint
  getTTFB(sendToAnalytics); // Time to First Byte
};

各メトリクスを個別に測定し、コールバック関数で送信処理を実行します。

React アプリケーションへの組み込み

Next.js を使用している場合、_app.tsx で Web Vitals を初期化するのが一般的です。

typescript// pages/_app.tsx
import type { AppProps } from 'next/app';
import { useEffect } from 'react';
import { reportWebVitals } from '@/utils/webVitals';

必要なモジュールをインポートします。

typescriptfunction MyApp({ Component, pageProps }: AppProps) {
  // アプリケーション起動時に Web Vitals 測定を開始
  useEffect(() => {
    reportWebVitals();
  }, []);

  return <Component {...pageProps} />;
}

export default MyApp;

useEffect フックを使用し、コンポーネントマウント時に測定を開始することで、全ページで自動的に計測されるようになります。

Performance API によるトレース実装

Web Vitals だけでは「何が」遅いかまではわかりません。そこで、Performance API を活用してより詳細なトレースを実装しましょう。

カスタムメジャーの作成

Performance API の markmeasure を使用して、特定の処理の実行時間を測定できます。

typescript// src/utils/performanceTrace.ts
export class PerformanceTrace {
  private markName: string;

  constructor(traceName: string) {
    this.markName = `${traceName}-start`;
    // 開始マークを記録
    performance.mark(this.markName);
  }

トレースの開始時点でマークを記録します。

typescript  // トレースを終了し、測定結果を取得
  end(): PerformanceMeasure | null {
    const endMarkName = `${this.markName}-end`;
    const measureName = this.markName.replace('-start', '');

    try {
      // 終了マークを記録
      performance.mark(endMarkName);

      // 開始から終了までの時間を測定
      performance.measure(measureName, this.markName, endMarkName);

      // 測定結果を取得
      const measure = performance.getEntriesByName(measureName)[0] as PerformanceMeasure;

      return measure;
    } catch (error) {
      console.error('Performance measurement failed:', error);
      return null;
    }
  }

終了時点でマークを記録し、開始マークとの差分から実行時間を計算します。

typescript  // マークをクリーンアップ(メモリリーク防止)
  cleanup(): void {
    const endMarkName = `${this.markName}-end`;
    const measureName = this.markName.replace('-start', '');

    performance.clearMarks(this.markName);
    performance.clearMarks(endMarkName);
    performance.clearMeasures(measureName);
  }
}

測定後はマークとメジャーをクリアし、メモリリークを防ぎます。

Emotion スタイル生成のトレース

Emotion のスタイル生成処理をトレースするためのヘルパー関数を作成しましょう。

typescript// src/utils/emotionTrace.ts
import { PerformanceTrace } from './performanceTrace';

先ほど作成したトレースクラスをインポートします。

typescript// スタイル生成をトレース
export const traceStyleGeneration = <T>(
  componentName: string,
  styleFunction: () => T
): T => {
  const trace = new PerformanceTrace(
    `emotion-style-${componentName}`
  );

  try {
    // スタイル生成処理を実行
    const result = styleFunction();
    return result;
  } finally {
    // 測定を終了
    const measure = trace.end();

    if (measure) {
      // 閾値を超えた場合は警告を出力
      if (measure.duration > 16) {
        // 1フレーム (16ms) を超える場合
        console.warn(
          `[Emotion Performance] ${componentName} style generation took ${measure.duration.toFixed(
            2
          )}ms`
        );
      }

      // 分析サーバーへ送信
      sendTraceData({
        componentName,
        duration: measure.duration,
        timestamp: Date.now(),
      });
    }

    trace.cleanup();
  }
};

スタイル生成時間が 16ms(1 フレーム)を超える場合に警告を出力することで、パフォーマンス問題を早期発見できます。

typescript// トレースデータを送信
const sendTraceData = (data: {
  componentName: string;
  duration: number;
  timestamp: number;
}) => {
  if (navigator.sendBeacon) {
    navigator.sendBeacon(
      '/api/traces',
      JSON.stringify(data)
    );
  }
};

トレースデータも Web Vitals と同様に、ビーコン API で確実に送信します。

React DevTools Profiler との連携

React DevTools Profiler を活用することで、コンポーネントのレンダリング性能を詳細に分析できます。

Profiler API の実装

React が提供する Profiler コンポーネントを使用して、レンダリングフェーズを測定します。

typescript// src/components/ProfiledComponent.tsx
import { Profiler, ProfilerOnRenderCallback } from 'react';

Profiler コンポーネントと、コールバック関数の型をインポートします。

typescript// プロファイリングコールバック
const onRenderCallback: ProfilerOnRenderCallback = (
  id, // プロファイラーの ID
  phase, // "mount" または "update"
  actualDuration, // 実際のレンダリング時間
  baseDuration, // メモ化なしの推定時間
  startTime, // レンダリング開始時刻
  commitTime, // コミット時刻
  interactions // このレンダリングに関連する操作
) => {
  // Emotion を使用しているコンポーネントの場合、閾値を厳しく設定
  const threshold = 10; // 10ms

  if (actualDuration > threshold) {
    console.warn(
      `[Profiler] ${id} (${phase}) took ${actualDuration.toFixed(
        2
      )}ms`
    );

    // 詳細情報を送信
    sendProfileData({
      id,
      phase,
      actualDuration,
      baseDuration,
      timestamp: commitTime,
    });
  }
};

レンダリング時間が閾値を超えた場合に、詳細情報をログ出力し、サーバーへ送信します。

typescript// プロファイル対象コンポーネントのラッパー
export const ProfiledComponent: React.FC<{
  id: string;
  children: React.ReactNode;
}> = ({ id, children }) => {
  return (
    <Profiler id={id} onRender={onRenderCallback}>
      {children}
    </Profiler>
  );
};

Profiler でラップすることで、子コンポーネントのレンダリング性能を自動的に測定できます。

typescript// プロファイルデータの送信
const sendProfileData = (data: {
  id: string;
  phase: string;
  actualDuration: number;
  baseDuration: number;
  timestamp: number;
}) => {
  if (navigator.sendBeacon) {
    navigator.sendBeacon(
      '/api/profiler',
      JSON.stringify(data)
    );
  }
};

以下の図は、Web Vitals とトレースを組み合わせた監視フローを示しています。

mermaidsequenceDiagram
    participant User as ユーザー
    participant App as React App
    participant Emotion as Emotion
    participant Vitals as Web Vitals
    participant Trace as Performance<br/>Trace
    participant API as 分析 API

    User->>App: ページアクセス
    App->>Vitals: 測定開始
    App->>Emotion: スタイル生成要求
    Trace->>Trace: mark(start)
    Emotion->>Emotion: CSS 生成
    Trace->>Trace: mark(end)
    Trace->>Trace: measure()
    Emotion->>App: スタイル適用
    App->>User: レンダリング完了
    Vitals->>API: LCP/FID/CLS 送信
    Trace->>API: 処理時間送信

図で理解できる要点:

  • Web Vitals とトレースが並行して動作
  • Emotion の各処理が測定対象
  • 分析 API へのデータ集約により一元管理

具体例

実践的な監視システムの構築

ここでは、実際のプロジェクトで使用できる包括的な監視システムを構築していきます。バックエンド API からフロントエンドの実装まで、段階的に解説しますね。

バックエンド:データ収集 API の実装

まず、フロントエンドから送信されるメトリクスデータを受け取る API を実装します。

typescript// pages/api/analytics.ts
import type { NextApiRequest, NextApiResponse } from 'next';

Next.js の API Routes の型定義をインポートします。

typescript// メトリクスデータの型定義
interface MetricData {
  name: string; // メトリクス名
  value: number; // 測定値
  rating: 'good' | 'needs-improvement' | 'poor';
  delta: number; // 差分
  id: string; // ユニーク ID
  navigationType: string; // ナビゲーションタイプ
  timestamp?: number; // タイムスタンプ
  userAgent?: string; // ユーザーエージェント
  url?: string; // ページ URL
}

受信するデータの構造を明確に定義します。

typescriptexport default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // POST メソッドのみ受け付ける
  if (req.method !== 'POST') {
    return res
      .status(405)
      .json({ error: 'Method not allowed' });
  }

  try {
    // リクエストボディをパース
    const metricData: MetricData = JSON.parse(req.body);

    // タイムスタンプと追加情報を付与
    const enrichedData = {
      ...metricData,
      timestamp: Date.now(),
      userAgent: req.headers['user-agent'],
      url: req.headers.referer,
    };

    // データベースまたはログサービスへ保存
    await saveMetric(enrichedData);

    // 成功レスポンス
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Failed to save metric:', error);
    res
      .status(500)
      .json({ error: 'Internal server error' });
  }
}

エラーハンドリングを適切に行い、データの保存に失敗しても監視が止まらないようにします。

typescript// データ保存処理(実装例)
async function saveMetric(data: MetricData) {
  // 実際の実装では、以下のようなストレージを使用
  // - PostgreSQL / MySQL
  // - MongoDB
  // - Google Analytics
  // - Datadog / New Relic などの APM ツール

  console.log('[Analytics] Metric saved:', data);

  // 例: データベースへの保存
  // await db.metrics.insert(data);
}

トレースデータ収集 API

同様に、Performance Trace のデータを受け取る API も実装します。

typescript// pages/api/traces.ts
import type { NextApiRequest, NextApiResponse } from 'next';

interface TraceData {
  componentName: string;
  duration: number;
  timestamp: number;
}

トレースデータ専用の型定義を作成します。

typescriptexport default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res
      .status(405)
      .json({ error: 'Method not allowed' });
  }

  try {
    const traceData: TraceData = JSON.parse(req.body);

    // 追加情報の付与
    const enrichedTrace = {
      ...traceData,
      userAgent: req.headers['user-agent'],
      url: req.headers.referer,
    };

    await saveTrace(enrichedTrace);

    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Failed to save trace:', error);
    res
      .status(500)
      .json({ error: 'Internal server error' });
  }
}

async function saveTrace(
  data: TraceData & { userAgent?: string; url?: string }
) {
  console.log('[Trace] Data saved:', data);
  // データベースへの保存処理
}

フロントエンド:監視対象コンポーネントの実装

実際に Emotion を使用したコンポーネントで、監視機能を組み込む例をご紹介します。

typescript// src/components/Button.tsx
import { css } from '@emotion/react';
import { traceStyleGeneration } from '@/utils/emotionTrace';

先ほど作成したトレース関数をインポートします。

typescript// ボタンコンポーネントの props 定義
interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'small' | 'medium' | 'large';
  children: React.ReactNode;
  onClick?: () => void;
}

型安全な props 定義により、誤った値の使用を防ぎます。

typescript// スタイル定義をトレース付きで生成
const getButtonStyles = (variant: ButtonProps['variant'], size: ButtonProps['size']) => {
  return traceStyleGeneration(`Button-${variant}-${size}`, () => {
    // ベーススタイル
    const baseStyles = css`
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-weight: 600;
      transition: all 0.2s ease-in-out;

      &:hover {
        opacity: 0.8;
      }

      &:active {
        transform: scale(0.98);
      }
    `;

トランジションやホバーエフェクトを含む基本スタイルを定義します。

typescript// バリアント別のスタイル
const variantStyles = {
  primary: css`
    background-color: #0070f3;
    color: white;
  `,
  secondary: css`
    background-color: #f5f5f5;
    color: #333;
  `,
  danger: css`
    background-color: #e00;
    color: white;
  `,
};

オブジェクト形式でバリアント別スタイルを管理することで、保守性が向上します。

typescript    // サイズ別のスタイル
    const sizeStyles = {
      small: css`
        padding: 8px 16px;
        font-size: 14px;
      `,
      medium: css`
        padding: 12px 24px;
        font-size: 16px;
      `,
      large: css`
        padding: 16px 32px;
        font-size: 18px;
      `,
    };

    // スタイルを結合して返す
    return [baseStyles, variantStyles[variant], sizeStyles[size]];
  });
};

配列形式でスタイルを返すことで、Emotion が効率的に結合できます。

typescript// ボタンコンポーネント本体
export const Button: React.FC<ButtonProps> = ({
  variant,
  size,
  children,
  onClick,
}) => {
  // スタイルを取得(トレース付き)
  const styles = getButtonStyles(variant, size);

  return (
    <button css={styles} onClick={onClick}>
      {children}
    </button>
  );
};

Profiler でラップした使用例

実際のページで Profiler を活用してコンポーネントを監視する例です。

typescript// pages/index.tsx
import { ProfiledComponent } from '@/components/ProfiledComponent';
import { Button } from '@/components/Button';

必要なコンポーネントをインポートします。

typescriptexport default function HomePage() {
  return (
    <div>
      <h1>パフォーマンス監視デモ</h1>

      {/* ボタングループを Profiler で監視 */}
      <ProfiledComponent id='button-group'>
        <div style={{ display: 'flex', gap: '8px' }}>
          <Button variant='primary' size='medium'>
            Primary
          </Button>
          <Button variant='secondary' size='medium'>
            Secondary
          </Button>
          <Button variant='danger' size='medium'>
            Danger
          </Button>
        </div>
      </ProfiledComponent>
    </div>
  );
}

Profiler でラップすることで、ボタングループ全体のレンダリング性能を自動測定できます。

ダッシュボードでの可視化

収集したデータを可視化するためのシンプルなダッシュボードを実装しましょう。

typescript// pages/dashboard.tsx
import { useEffect, useState } from 'react';

React の基本的なフックをインポートします。

typescript// メトリクスサマリーの型定義
interface MetricsSummary {
  lcp: { value: number; rating: string };
  fid: { value: number; rating: string };
  cls: { value: number; rating: string };
  slowestComponents: Array<{
    name: string;
    avgDuration: number;
  }>;
}

ダッシュボードに表示するデータの型を定義します。

typescriptexport default function Dashboard() {
  const [metrics, setMetrics] = useState<MetricsSummary | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // メトリクスデータを取得
    fetch('/api/metrics-summary')
      .then(res => res.json())
      .then(data => {
        setMetrics(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('Failed to fetch metrics:', error);
        setLoading(false);
      });
  }, []);

コンポーネントマウント時にデータを取得します。

typescript  if (loading) {
    return <div>読み込み中...</div>;
  }

  if (!metrics) {
    return <div>データの取得に失敗しました</div>;
  }

  return (
    <div style={{ padding: '24px' }}>
      <h1>パフォーマンスダッシュボード</h1>

      {/* Web Vitals セクション */}
      <section>
        <h2>Core Web Vitals</h2>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr>
              <th style={{ textAlign: 'left', padding: '8px', borderBottom: '2px solid #ddd' }}>指標</th>
              <th style={{ textAlign: 'left', padding: '8px', borderBottom: '2px solid #ddd' }}>値</th>
              <th style={{ textAlign: 'left', padding: '8px', borderBottom: '2px solid #ddd' }}>評価</th>
            </tr>
          </thead>

表組みでわかりやすくデータを表示します。

typescript          <tbody>
            <tr>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>LCP</td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>
                {metrics.lcp.value.toFixed(2)} ms
              </td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>
                <span style={{
                  color: metrics.lcp.rating === 'good' ? 'green' :
                         metrics.lcp.rating === 'needs-improvement' ? 'orange' : 'red'
                }}>
                  {metrics.lcp.rating}
                </span>
              </td>
            </tr>
            <tr>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>FID</td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>
                {metrics.fid.value.toFixed(2)} ms
              </td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>
                <span style={{
                  color: metrics.fid.rating === 'good' ? 'green' :
                         metrics.fid.rating === 'needs-improvement' ? 'orange' : 'red'
                }}>
                  {metrics.fid.rating}
                </span>
              </td>
            </tr>
            <tr>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>CLS</td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>
                {metrics.cls.value.toFixed(3)}
              </td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>
                <span style={{
                  color: metrics.cls.rating === 'good' ? 'green' :
                         metrics.cls.rating === 'needs-improvement' ? 'orange' : 'red'
                }}>
                  {metrics.cls.rating}
                </span>
              </td>
            </tr>
          </tbody>
        </table>
      </section>

評価に応じて色分けすることで、視覚的に問題を把握しやすくなります。

typescript      {/* 遅いコンポーネント一覧 */}
      <section style={{ marginTop: '32px' }}>
        <h2>処理時間が長いコンポーネント(Top 5)</h2>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr>
              <th style={{ textAlign: 'left', padding: '8px', borderBottom: '2px solid #ddd' }}>#</th>
              <th style={{ textAlign: 'left', padding: '8px', borderBottom: '2px solid #ddd' }}>コンポーネント名</th>
              <th style={{ textAlign: 'left', padding: '8px', borderBottom: '2px solid #ddd' }}>平均処理時間</th>
            </tr>
          </thead>
          <tbody>
            {metrics.slowestComponents.map((component, index) => (
              <tr key={component.name}>
                <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>{index + 1}</td>
                <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>{component.name}</td>
                <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>
                  {component.avgDuration.toFixed(2)} ms
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </section>
    </div>
  );
}

ボトルネックとなっているコンポーネントをランキング形式で表示します。

以下の図は、監視システム全体のデータフローを示しています。

mermaidflowchart TB
    frontend["フロントエンド"] -->|"Web Vitals<br/>送信"| api1["/api/analytics"]
    frontend -->|"Trace<br/>送信"| api2["/api/traces"]
    frontend -->|"Profiler<br/>送信"| api3["/api/profiler"]

    api1 -->|"保存"| db[("データベース")]
    api2 -->|"保存"| db
    api3 -->|"保存"| db

    db -->|"集計<br/>クエリ"| summary["/api/metrics-summary"]
    summary -->|"JSON"| dashboard["ダッシュボード<br/>ページ"]

    dashboard -->|"可視化"| user["開発者"]
    user -->|"ボトルネック<br/>特定"| optimize["最適化<br/>作業"]
    optimize -->|"改善"| frontend

図で理解できる要点:

  • 複数のデータソースから一元的に収集
  • データベースで集約・分析
  • ダッシュボードで可視化し改善サイクルを回す

最適化の実践例

監視により特定されたボトルネックを、実際に最適化する例をご紹介します。

スタイルのメモ化

動的スタイルを毎回再計算するのではなく、useMemo でメモ化することでパフォーマンスが向上します。

typescript// 最適化前:毎回スタイルが再計算される
const BadButton = ({ color, size }) => {
  const styles = css`
    background-color: ${color};
    padding: ${size === 'large' ? '16px 32px' : '8px 16px'};
  `;

  return <button css={styles}>Click</button>;
};

このコンポーネントでは、親が再レンダリングされるたびにスタイルが再生成されてしまいます。

typescript// 最適化後:props が変わらない限りスタイルを再利用
import { useMemo } from 'react';

const GoodButton = ({ color, size }) => {
  const styles = useMemo(
    () => css`
      background-color: ${color};
      padding: ${size === 'large'
        ? '16px 32px'
        : '8px 16px'};
    `,
    [color, size]
  ); // color と size が変わった時だけ再計算

  return <button css={styles}>Click</button>;
};

useMemo により、依存配列の値が変わらない限りキャッシュされたスタイルが使用されます。

静的スタイルの外部定義

コンポーネント外で静的にスタイルを定義することで、さらにパフォーマンスが向上するでしょう。

typescript// スタイルをコンポーネント外で定義
const buttonBaseStyles = css`
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
`;

const variantStyles = {
  primary: css`
    background-color: #0070f3;
    color: white;
  `,
  secondary: css`
    background-color: #f5f5f5;
    color: #333;
  `,
};

これらのスタイルは一度だけ生成され、すべてのインスタンスで共有されます。

typescript// コンポーネントでは定義済みスタイルを組み合わせるだけ
const OptimizedButton = ({ variant }) => {
  return (
    <button
      css={[buttonBaseStyles, variantStyles[variant]]}
    >
      Click
    </button>
  );
};

スタイルの組み合わせのみを行うため、処理コストが大幅に削減されます。

コンポーネントの分割とメモ化

大きなコンポーネントを小さく分割し、React.memo で最適化する方法です。

typescript// 最適化前:一つの大きなコンポーネント
const LargeComponent = ({ items, onItemClick, theme }) => {
  return (
    <div
      css={css`
        background: ${theme.background};
      `}
    >
      {items.map((item) => (
        <div
          key={item.id}
          css={css`
            padding: 16px;
            color: ${theme.text};
            border-bottom: 1px solid ${theme.border};
          `}
          onClick={() => onItemClick(item.id)}
        >
          {item.name}
        </div>
      ))}
    </div>
  );
};

このコンポーネントでは、theme が変わるとすべてのアイテムが再レンダリングされてしまいます。

typescript// 最適化後:アイテムを別コンポーネントに分割
import { memo } from 'react';

const Item = memo<{
  item: any;
  theme: any;
  onClick: () => void;
}>(({ item, theme, onClick }) => {
  const styles = useMemo(
    () => css`
      padding: 16px;
      color: ${theme.text};
      border-bottom: 1px solid ${theme.border};
    `,
    [theme]
  );

  return (
    <div css={styles} onClick={onClick}>
      {item.name}
    </div>
  );
});

Item.displayName = 'Item';

memo により、props が変わらないアイテムは再レンダリングをスキップできます。

typescript// 親コンポーネント
const OptimizedComponent = ({
  items,
  onItemClick,
  theme,
}) => {
  const containerStyles = useMemo(
    () => css`
      background: ${theme.background};
    `,
    [theme]
  );

  return (
    <div css={containerStyles}>
      {items.map((item) => (
        <Item
          key={item.id}
          item={item}
          theme={theme}
          onClick={() => onItemClick(item.id)}
        />
      ))}
    </div>
  );
};

コンポーネントを分割することで、変更の影響範囲を最小限に抑えられます。

まとめ

本記事では、Emotion を使用した React アプリケーションにおけるパフォーマンス監視運用について、Web Vitals とトレース技術を組み合わせた実践的なアプローチをご紹介しました。

重要なポイントを振り返りましょう。

監視の基礎

  • Web Vitals(LCP、FID、CLS)でユーザー体験を定量化
  • Performance API でスタイル生成処理を詳細にトレース
  • React Profiler でコンポーネントレンダリングを分析

実装のベストプラクティス

  • web-vitals ライブラリで簡単に計測を開始できる
  • カスタムトレースでボトルネックを特定する
  • データ収集 API とダッシュボードで可視化する

最適化の手法

  • useMemo でスタイルをメモ化し再計算を防ぐ
  • 静的スタイルはコンポーネント外で定義する
  • React.memo でコンポーネントの不要な再レンダリングを抑制する

パフォーマンス監視は一度設定すれば終わりではなく、継続的な改善サイクルを回していくことが大切です。本記事で紹介した手法を実践することで、ユーザーにとって快適なアプリケーションを提供できるようになるでしょう。

まずは小さく始めて、段階的に監視範囲を広げていくことをお勧めします。実際の数値を見ながら最適化を進めることで、着実にパフォーマンスが向上していく実感を得られますよ。

皆さんのプロジェクトでも、ぜひこの監視システムを活用してみてください。

関連リンク