Emotion のパフォーマンス監視運用:Web Vitals× トレースでボトルネック特定
React アプリケーションでスタイリングライブラリの Emotion を利用している開発者の皆さん、こんな悩みはありませんか。「ページの読み込みが遅い」「ユーザー体験が低下している」「どこがボトルネックなのかわからない」――そんな課題を解決するため、本記事では Emotion のパフォーマンス監視運用について、Web Vitals とトレース技術を組み合わせた実践的なアプローチをご紹介します。
初心者の方でも実装できるよう、具体的なコード例と図解を交えながら、段階的に解説していきますね。実際の運用で使えるノウハウが満載ですので、ぜひ最後までお付き合いください。
背景
Emotion とパフォーマンスの関係性
Emotion は CSS-in-JS ライブラリとして、React コンポーネント内でスタイルを記述できる強力なツールです。動的なスタイリングやテーマ機能を簡単に実装できる一方で、実行時にスタイルを生成するという特性から、パフォーマンスへの影響を考慮する必要があります。
特に以下のような場面で、パフォーマンスボトルネックが発生しやすくなるでしょう。
- コンポーネントの再レンダリングが頻繁に発生する
- 大量のスタイル定義が動的に生成される
- ネストが深い複雑なコンポーネント構造
Web Vitals とは何か
Web Vitals は、Google が提唱するユーザー体験を測定するための指標群です。これらの指標は、実際のユーザーがウェブサイトを利用する際の体験を数値化したものとなっています。
主要な 3 つの Core Web Vitals は次のとおりです。
| # | 指標名 | 測定内容 | 目標値 |
|---|---|---|---|
| 1 | LCP (Largest Contentful Paint) | 最大コンテンツの描画時間 | 2.5 秒以内 |
| 2 | FID (First Input Delay) | 初回入力遅延 | 100 ミリ秒以内 |
| 3 | CLS (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>
);
このようなコンポーネントでは、color や size が変更されるたびにスタイルが再生成されます。
コンポーネント再レンダリングとスタイル更新の連鎖
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 の mark と measure を使用して、特定の処理の実行時間を測定できます。
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でコンポーネントの不要な再レンダリングを抑制する
パフォーマンス監視は一度設定すれば終わりではなく、継続的な改善サイクルを回していくことが大切です。本記事で紹介した手法を実践することで、ユーザーにとって快適なアプリケーションを提供できるようになるでしょう。
まずは小さく始めて、段階的に監視範囲を広げていくことをお勧めします。実際の数値を見ながら最適化を進めることで、着実にパフォーマンスが向上していく実感を得られますよ。
皆さんのプロジェクトでも、ぜひこの監視システムを活用してみてください。
関連リンク
articleEmotion のパフォーマンス監視運用:Web Vitals× トレースでボトルネック特定
articleEmotion で FOUC が出る原因と解決策:挿入順序/SSR 抽出/プリロードの総点検
articleEmotion で B2B 管理画面を高速構築:テーブル/フィルタ/フォームの型安全化
articleEmotion で「ボタンの設計システム」を構築:サイズ/色/状態/アイコンを型安全に
articleEmotion の「変種(variants)」設計パターン:props→ スタイルの型安全マッピング
articleEmotion チートシート:css/styled/Global/Theme の即戦力スニペット 20
articlegpt-oss アーキテクチャを分解図で理解する:推論ランタイム・トークナイザ・サービング層の役割
articlePHP で社内業務自動化:CSV→DB 取込・定期バッチ・Slack 通知の実例
articleGPT-5 × Cloudflare Workers/Edge:低遅延サーバーレスのスターターガイド
articleNotebookLM と Notion AI/ChatGPT の比較:根拠提示とソース管理の違い
articleFlutter で ToDo アプリを 90 分で作る:状態管理・永続化・ダークモード対応
articleEmotion のパフォーマンス監視運用:Web Vitals× トレースでボトルネック特定
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来