mcpField
P
performance-audit

performance-audit

github.com/mgd34msuテスト・品質659 回インストール10 週間前に更新
リポジトリ

performance-audit について

performance-audit は、Web アプリの性能を調べる開発者向けスキルです。バンドル、データベース、レンダリング、ネットワーク、メモリを段階的に確認します。

主な機能

  1. 01段階的な Web 性能監査
  2. 02バンドルと依存関係の分析
  3. 03データベースクエリとインデックスの確認
  4. 04レンダリングと再描画の調査
  5. 05ネットワークとキャッシュの確認

活用シーン

  1. 01デプロイ前の性能レビュー
  2. 02ページ表示の遅さの調査
  3. 03大規模機能追加後の性能確認
  4. 04トラフィック拡大前のアプリ評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mgd34msu/goodvibes-plugin --skill performance-audit

インストール後、Claude Code で/performance-auditを入力して読み込まれたか確認します。

同梱ファイルreferences/performance-patterns.mdscripts/validate-performance-audit.sh

詳細

リソース

scripts/
  validate-performance-audit.sh
references/
  performance-patterns.md

パフォーマンス監査

このスキルでは、Web アプリケーションの包括的なパフォーマンス監査を実施し、ボトルネック、最適化の機会、パフォーマンスの退行を特定する方法を説明します。本番環境へのデプロイの準備、パフォーマンス問題の調査、定期的なパフォーマンスレビューを行う際に使用してください。

このスキルを使用するタイミング

  • デプロイ前のパフォーマンスレビューを実施するとき
  • ページの読み込みが遅い、またはユーザーエクスペリエンスが低い原因を調査するとき
  • 既存のアプリケーションの定期的なパフォーマンス監査を実施するとき
  • 大規模な機能追加後にパフォーマンスを検証するとき
  • トラフィックの拡大や負荷テストに備えるとき
  • SEO とユーザーエクスペリエンスのために Core Web Vitals を最適化するとき

監査の方法

体系的なパフォーマンス監査は、次のフェーズに沿って実施します。

フェーズ 1:事前調査

目的: アプリケーションのアーキテクチャ、技術スタック、パフォーマンスに関わる範囲を把握します。

discover を使用して、パフォーマンス上重要なコードをマッピングします:

discover:
  queries:
    - id: bundle_imports
      type: grep
      pattern: "^import.*from ['\"].*['\"]$"
      glob: "src/**/*.{ts,tsx,js,jsx}"
    - id: database_queries
      type: grep
      pattern: "(prisma\\.|db\\.|query\\(|findMany|findUnique|create\\(|update\\()"
      glob: "**/*.{ts,tsx,js,jsx}"
    - id: component_files
      type: glob
      patterns: ["src/components/**/*.{tsx,jsx}", "src/app/**/*.{tsx,jsx}"]
    - id: api_routes
      type: glob
      patterns: ["src/app/api/**/*.{ts,tsx}", "pages/api/**/*.{ts,tsx}"]
  verbosity: files_only

重要なコンポーネントを特定します:

  • ページのエントリーポイントとルートハンドラー
  • サイズの大きい npm 依存関係(moment.js、lodash など)
  • データベースへのアクセスパターンと ORM の使用状況
  • 画像やメディアのアセット
  • API エンドポイントとデータ取得ロジック
  • クライアントサイドレンダリングとサーバーコンポーネントの使い分け
フェーズ 2:バンドル分析

目的: サイズの大きい依存関係、コード分割の機会、不要なコードを特定します。

バンドルサイズを確認する

バンドルアナライザーを実行します:

precision_exec:
  commands:
    - cmd: "npm run build"
      timeout_ms: 120000
    - cmd: "npx @next/bundle-analyzer"
      timeout_ms: 60000
  verbosity: standard

よくある問題:

  • すべてのページでインポートされるサイズの大きい依存関係(moment.js、lodash、チャートライブラリなど)
  • 同じパッケージの複数のバージョン(npm ls <package> で確認)
  • 使用されていない依存関係が引き続きバンドルされる(ツリーシェイキングの失敗)
  • SVG アイコンをスプライトシートではなくコンポーネントとしてインポートしている
  • 個々のコンポーネントではなく、UI ライブラリ全体をインポートしている
サイズの大きいインポートを見つける

パフォーマンスへの影響が大きい一般的なパッケージを検索します:

precision_grep:
  queries:
    - id: heavy_deps
      pattern: "import.*from ['\"](moment|lodash|date-fns|rxjs|@material-ui|antd|chart\\.js)['\"]"
      glob: "**/*.{ts,tsx,js,jsx}"
    - id: full_library_imports
      pattern: "import .* from ['\"](lodash|@mui/material|react-icons)['\"]$"
      glob: "**/*.{ts,tsx,js,jsx}"
  output:
    format: locations

最適化の方法:

悪い例 - ライブラリ全体をインポートする:

import _ from 'lodash'; // 71KB gzipped
import * as Icons from 'react-icons/fa'; // 500+ icons

良い例 - 特定のモジュールをインポートする:

import debounce from 'lodash/debounce'; // 2KB gzipped
import { FaUser, FaCog } from 'react-icons/fa'; // Only what you need

さらに良い例 - 最新の代替手段を使う:

// Instead of moment.js (72KB), use date-fns (13KB) or native Intl
import { format } from 'date-fns';

// Or native APIs
const formatted = new Intl.DateTimeFormat('en-US').format(date);
コード分割を確認する

動的インポートを探します:

precision_grep:
  queries:
    - id: dynamic_imports
      pattern: "(import\\(|React\\.lazy|next/dynamic)"
      glob: "**/*.{ts,tsx,js,jsx}"
    - id: large_components
      pattern: "export (default )?(function|const).*\\{[\\s\\S]{2000,}"
      glob: "src/components/**/*.{tsx,jsx}"
      multiline: true
  output:
    format: files_only

コード分割のパターン:

Next.js の動的インポート:

import dynamic from 'next/dynamic';

// Client-only components (no SSR)
const ChartComponent = dynamic(() => import('./Chart'), {
  ssr: false,
  loading: () => <Spinner />,
});

// Route-based code splitting
const AdminPanel = dynamic(() => import('./AdminPanel'));

クライアントコンポーネントでの React.lazy:

import { lazy, Suspense } from 'react';

const HeavyModal = lazy(() => import('./HeavyModal'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <HeavyModal />
    </Suspense>
  );
}
フェーズ 3:データベースのパフォーマンス

目的: N+1 クエリを解消し、不足しているインデックスを追加して、クエリパターンを最適化します。

N+1 クエリのパターンを検出する

ループ内の逐次クエリを検索します:

precision_grep:
  queries:
    - id: potential_n_plus_one
      pattern: "(map|forEach).*await.*(findUnique|findFirst|findMany)"
      glob: "**/*.{ts,tsx,js,jsx}"
    - id: missing_includes
      pattern: "findMany\\(\\{[^}]*where[^}]*\\}\\)"
      glob: "**/*.{ts,tsx,js,jsx}"
  output:
    format: context
    context_before: 3
    context_after: 3

よくある N+1 パターン:

悪い例 - N+1 クエリ:

const posts = await db.post.findMany();

// Runs 1 query per post!
const postsWithAuthors = await Promise.all(
  posts.map(async (post) => ({
    ...post,
    author: await db.user.findUnique({ where: { id: post.authorId } }),
  }))
);

良い例 - include による先読み:

const posts = await db.post.findMany({
  include: {
    author: true,
    comments: {
      include: {
        user: true,
      },
    },
  },
});

さらに良い例 - 必要なフィールドのみを明示的に select する:

const posts = await db.post.findMany({
  select: {
    id: true,
    title: true,
    author: {
      select: {
        id: true,
        name: true,
        avatar: true,
      },
    },
  },
});
不足しているインデックスを確認する

Prisma スキーマでインデックスの設定状況を確認します:

precision_read:
  files:
    - path: "prisma/schema.prisma"
      extract: content
  verbosity: standard

次に WHERE 句を検索します:

precision_grep:
  queries:
    - id: where_clauses
      pattern: "where:\\s*\\{\\s*(\\w+):"
      glob: "**/*.{ts,tsx,js,jsx}"
  output:
    format: locations

インデックスの最適化:

頻繁にクエリされるフィールドにインデックスを追加します:

model Post {
  id        String   @id @default(cuid())
  title     String
  published Boolean  @default(false)
  authorId  String
  createdAt DateTime @default(now())
  
  // Single column indexes
  @@index([authorId])
  @@index([createdAt])
  
  // Composite index for common query pattern
  @@index([published, createdAt(sort: Desc)])
}

インデックスを追加する場合:

  • WHERE 句や結合で使用される外部キー
  • ORDER BY 句で使用されるフィールド
  • ページネーション(カーソルベース)を伴う WHERE 句で使用されるフィールド
  • 複数フィールドのクエリ向けの複合インデックス

インデックスを追加しない場合:

  • カーディナリティの低いブール型フィールド(複合インデックスの一部である場合を除く)
  • 頻繁に変更されるフィールド(書き込みが遅くなります)
  • 行数が非常に少ないテーブル
コネクションプーリングを最適化する

データベース接続の設定を確認します:

precision_grep:
  queries:
    - id: prisma_config
      pattern: "PrismaClient\\(.*\\{[\\s\\S]*?\\}"
      glob: "**/*.{ts,tsx,js,jsx}"
      multiline: true
    - id: connection_string
      pattern: "connection_limit=|pool_timeout=|connect_timeout="
      glob: ".env*"
  output:
    format: context
    context_before: 3
    context_after: 3

最適なコネクションプール設定:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
  log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});

export { prisma };

コネクションプーリングを使用する DATABASE_URL:

# For serverless (Vercel, AWS Lambda) - use connection pooler
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=10"

# For long-running servers - higher limits
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=30"
フェーズ 4:レンダリングのパフォーマンス

目的: 不要な再レンダリングを解消し、コンポーネントのレンダリングを最適化します。

不要な再レンダリングを検出する

メモ化の不足を探します:

注: 以下のパターンは、単一行を対象とした検出の近似です。複数行にわたるコンポーネント定義は、手動での確認が必要になる場合があります。

precision_grep:
  queries:
    - id: missing_memo
      pattern: "(const \\w+ = \\{|const \\w+ = \\[|const \\w+ = \\(.*\\) =>)(?!.*useMemo)"
      glob: "src/components/**/*.{tsx,jsx}"
    - id: missing_callback
      pattern: "(onChange|onClick|onSubmit)=\\{.*=>(?!.*useCallback)"
      glob: "src/components/**/*.{tsx,jsx}"
    - id: memo_usage
      pattern: "(useMemo|useCallback|React\\.memo)"
      glob: "**/*.{tsx,jsx}"
  output:
    format: files_only

メモ化のパターン:

悪い例 - レンダリングのたびにオブジェクトを再生成する:

function UserProfile({ userId }: Props) {
  const user = useUser(userId);
  
  // New object reference on every render!
  const config = {
    showEmail: true,
    showPhone: false,
  };
  
  return <UserCard user={user} config={config} />;
}

良い例 - 安定したオブジェクトをメモ化する:

function UserProfile({ userId }: Props) {
  const user = useUser(userId);
  
  const config = useMemo(
    () => ({
      showEmail: true,
      showPhone: false,
    }),
    [] // Empty deps - never changes
  );
  
  return <UserCard user={user} config={config} />;
}

コールバックをメモ化する:

function SearchBar({ onSearch }: Props) {
  const [query, setQuery] = useState('');
  
  // Memoize callback to prevent child re-renders
  const handleSubmit = useCallback(() => {
    onSearch(query);
  }, [query, onSearch]);
  
  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
    </form>
  );
}

コストの高いコンポーネントをメモ化する:

const ExpensiveChart = React.memo(
  function ExpensiveChart({ data }: Props) {
    // Heavy computation or rendering
    return <Chart data={data} />;
  },
  (prevProps, nextProps) => {
    // Custom comparison - only re-render if data changed
    return prevProps.data === nextProps.data;
  }
);
仮想リストの使用状況を確認する

仮想化されていない大きなリストを検索します:

precision_grep:
  queries:
    - id: large_maps
      pattern: "\\{.*\\.map\\(.*=>\\s*<(?!Virtualized)(?!VirtualList)"
      glob: "src/**/*.{tsx,jsx}"
    - id: virtualization
      pattern: "(react-window|react-virtualized|@tanstack/react-virtual)"
      glob: "package.json"
  output:
    format: locations

仮想リストの実装:

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50, // Estimated row height
    overscan: 5, // Render 5 extra rows for smooth scrolling
  });
  
  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.index}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <ItemRow item={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}
フェーズ 5:ネットワークの最適化

目的: リクエストのウォーターフォールを減らし、キャッシュを有効にして、アセット配信を最適化します。

リクエストのウォーターフォールを検出する

順次データ取得を検索します:

precision_grep:
  queries:
    - id: sequential_fetches
      pattern: "await fetch.*\\n.*await fetch"
      glob: "**/*.{ts,tsx,js,jsx}"
      multiline: true
    - id: use_effect_fetches
      pattern: "useEffect\\(.*fetch"
      glob: "**/*.{tsx,jsx}"
  output:
    format: context
    context_before: 3
    context_after: 3

ウォーターフォールの最適化:

悪い例 - リクエストを順次実行:

async function loadDashboard() {
  const userRes = await fetch('/api/user');
  if (!userRes.ok) throw new Error(`Failed to fetch user: ${userRes.status}`);
  const user = await userRes.json();
  
  const postsRes = await fetch(`/api/posts?userId=${user.id}`);
  if (!postsRes.ok) throw new Error(`Failed to fetch posts: ${postsRes.status}`);
  const posts = await postsRes.json();
  
  const commentsRes = await fetch(`/api/comments?userId=${user.id}`);
  if (!commentsRes.ok) throw new Error(`Failed to fetch comments: ${commentsRes.status}`);
  const comments = await commentsRes.json();
  
  return { user, posts, comments };
}

良い例 - リクエストを並列実行:

async function loadDashboard(userId: string) {
  const [user, posts, comments] = await Promise.all([
    fetch(`/api/user/${userId}`).then(async r => {
      if (!r.ok) throw new Error(`Failed to fetch user: ${r.status}`);
      return r.json();
    }),
    fetch(`/api/posts?userId=${userId}`).then(async r => {
      if (!r.ok) throw new Error(`Failed to fetch posts: ${r.status}`);
      return r.json();
    }),
    fetch(`/api/comments?userId=${userId}`).then(async r => {
      if (!r.ok) throw new Error(`Failed to fetch comments: ${r.status}`);
      return r.json();
    }),
  ]);
  
  return { user, posts, comments };
}

最良の例 - サーバー側でデータを集約:

// Single API endpoint that aggregates data server-side
async function loadDashboard(userId: string) {
  const response = await fetch(`/api/dashboard?userId=${userId}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch dashboard: ${response.status}`);
  }
  const data = await response.json();
  return data;
}
キャッシュヘッダーを確認する

キャッシュ設定を確認します:

precision_grep:
  queries:
    - id: cache_headers
      pattern: "(Cache-Control|ETag|max-age|stale-while-revalidate)"
      glob: "**/*.{ts,tsx,js,jsx}"
    - id: next_revalidate
      pattern: "(revalidate|force-cache|no-store)"
      glob: "src/app/**/*.{ts,tsx}"
  output:
    format: locations

Next.js のキャッシュ戦略:

静的データ(更新頻度が低い):

// app/products/page.tsx
export const revalidate = 3600; // Revalidate every hour

export default async function ProductsPage() {
  const products = await db.product.findMany();
  return <ProductList products={products} />;
}

動的データ(リアルタイム):

// app/api/posts/route.ts
export async function GET() {
  const posts = await db.post.findMany();
  
  return Response.json(posts, {
    headers: {
      'Cache-Control': 'private, max-age=60, stale-while-revalidate=300',
    },
  });
}

キャッシュを無効にする:

// app/api/user/route.ts
export const dynamic = 'force-dynamic';

export async function GET() {
  const user = await getCurrentUser();
  return Response.json(user);
}
画像の読み込みを最適化する

画像最適化の状況を確認します:

precision_grep:
  queries:
    - id: img_tags
      pattern: "<img\\s+"
      glob: "**/*.{tsx,jsx,html}"
    - id: next_image
      pattern: "(next/image|Image\\s+from)"
      glob: "**/*.{tsx,jsx}"
  output:
    format: files_only

画像最適化のパターン:

悪い例 - 最適化されていない画像:

<img src="/large-photo.jpg" alt="Photo" />

良い例 - Next.js Image コンポーネント:

import Image from 'next/image';

<Image
  src="/large-photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  loading="lazy"
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,..."
/>

レスポンシブ画像:

<Image
  src="/photo.jpg"
  alt="Photo"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  style={{ objectFit: 'cover' }}
/>
フェーズ 6:メモリ管理

目的: メモリリークを検出し、ガベージコレクションを最適化します。

メモリリークのパターンを見つける

クリーンアップの問題を検索します:

precision_grep:
  queries:
    - id: missing_cleanup
      pattern: "useEffect\\(.*\\{[^}]*addEventListener(?!.*return.*removeEventListener)"
      glob: "**/*.{tsx,jsx}"
      multiline: true
    - id: interval_leaks
      pattern: "(setInterval|setTimeout)(?!.*clear)"
      glob: "**/*.{tsx,jsx}"
    - id: subscription_leaks
      pattern: "subscribe\\((?!.*unsubscribe)"
      glob: "**/*.{ts,tsx,js,jsx}"
  output:
    format: context
    context_before: 3
    context_after: 3

メモリリークのパターン:

悪い例 - イベントリスナーのリーク:

useEffect(() => {
  window.addEventListener('resize', handleResize);
  // Missing cleanup!
}, []);

良い例 - 適切なクリーンアップ:

useEffect(() => {
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

悪い例 - インターバルのリーク:

useEffect(() => {
  const interval = setInterval(() => {
    fetchUpdates();
  }, 5000);
  // Missing cleanup!
}, []);

良い例 - インターバルをクリアする:

useEffect(() => {
  const interval = setInterval(() => {
    fetchUpdates();
  }, 5000);
  
  return () => {
    clearInterval(interval);
  };
}, []);

悪い例 - 中断処理のない fetch:

useEffect(() => {
  fetch('/api/data')
    .then(r => r.json() as Promise<DataType>)
    .then(setData)
    .catch(console.error);
  // Missing abort on unmount!
}, []);

良い例 - AbortController によるクリーンアップ:

useEffect(() => {
  const controller = new AbortController();
  
  fetch('/api/data', { signal: controller.signal })
    .then(r => r.json() as Promise<DataType>)
    .then(setData)
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err);
    });
  
  return () => controller.abort();
}, []);

キャッシュ向けの WeakRef パターン:

class ImageCache {
  private cache = new Map<string, WeakRef<HTMLImageElement>>();
  
  get(url: string): HTMLImageElement | undefined {
    const ref = this.cache.get(url);
    return ref?.deref();
  }
  
  set(url: string, img: HTMLImageElement): void {
    this.cache.set(url, new WeakRef(img));
  }
}
フェーズ 7:サーバーサイドのパフォーマンス

目的: SSR、ストリーミング、エッジ関数のパフォーマンスを最適化します。

ブロッキングするサーバーコンポーネントを確認する

遅いサーバーコンポーネントを検索します:

precision_grep:
  queries:
    - id: server_components
      pattern: "export default async function.*Page"
      glob: "src/app/**/page.{tsx,jsx}"
    - id: blocking_awaits
      pattern: "const.*await.*\\n.*const.*await"
      glob: "src/app/**/*.{tsx,jsx}"
      multiline: true
  output:
    format: locations

Suspense によるストリーミング:

悪い例 - ページ全体をブロック:

// app/dashboard/page.tsx
export default async function DashboardPage() {
  const user = await getUser();
  const posts = await getPosts(); // Blocks entire page!
  const analytics = await getAnalytics(); // Blocks entire page!
  
  return (
    <div>
      <UserProfile user={user} />
      <PostList posts={posts} />
      <Analytics data={analytics} />
    </div>
  );
}

良い例 - Suspense によるストリーミング:

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default async function DashboardPage() {
  // Only block on critical data
  const user = await getUser();
  
  return (
    <div>
      <UserProfile user={user} />
      
      <Suspense fallback={<PostListSkeleton />}>
        <PostList />
      </Suspense>
      
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Analytics />
      </Suspense>
    </div>
  );
}

// Separate component for async data
async function PostList() {
  const posts = await getPosts();
  return <div>{/* render posts */}</div>;
}
エッジ関数を最適化する

エッジランタイムの使用状況を確認します:

precision_grep:
  queries:
    - id: edge_config
      pattern: "export const runtime = ['\"](edge|nodejs)['\"]"
      glob: "**/*.{ts,tsx}"
    - id: edge_incompatible
      pattern: "(fs\\.|path\\.|process\\.cwd)"
      glob: "**/api/**/*.{ts,tsx}"
  output:
    format: locations

エッジランタイムのベストプラクティス:

// app/api/geo/route.ts
export const runtime = 'edge';

export async function GET(request: Request) {
  // Access edge-specific APIs
  const geo = request.headers.get('x-vercel-ip-country');
  
  return Response.json({
    country: geo,
    timestamp: Date.now(),
  });
}
フェーズ 8:Core Web Vitals

目的: LCP、INP、CLS を測定して最適化します。

Web Vitals を測定する

Web Vitals のレポート機能を追加します:

// app/layout.tsx or app/web-vitals.tsx
'use client';

import { useEffect } from 'react';
import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals';

import type { Metric } from 'web-vitals';

function sendToAnalytics(metric: Metric) {
  const body = JSON.stringify(metric);
  const url = '/api/analytics';
  
  if (navigator.sendBeacon) {
    navigator.sendBeacon(url, body);
  } else {
    fetch(url, { body, method: 'POST', keepalive: true });
  }
}

export function WebVitals() {
  useEffect(() => {
    onCLS(sendToAnalytics);
    onFCP(sendToAnalytics);
    onINP(sendToAnalytics);
    onLCP(sendToAnalytics);
    onTTFB(sendToAnalytics);
  }, []);
  
  return null;
}
LCP(Largest Contentful Paint)を最適化する

目標:< 2.5s

LCP に関する一般的な問題:

  • 大きなヒーロー画像が最適化されていない
  • Web フォントがレンダリングをブロックしている
  • サーバーサイドレンダリングが遅すぎる
  • リソースの優先順位付けがない

最適化:

// Priority image loading
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={630}
  priority // Preload this image!
/>

// Font optimization
<link
  rel="preload"
  href="/fonts/inter.woff2"
  as="font"
  type="font/woff2"
  crossOrigin="anonymous"
/>
INP(Interaction to Next Paint)を最適化する

目標:< 200ms

INP に関する一般的な問題:

  • 重い JavaScript がメインスレッドをブロックしている
  • イベントハンドラーの処理負荷が高い
  • レイアウトスラッシング

最適化:

// Debounce expensive handlers
import { useDebouncedCallback } from 'use-debounce';

function SearchInput() {
  const debouncedSearch = useDebouncedCallback(
    (value: string) => {
      performSearch(value);
    },
    300 // Wait 300ms after user stops typing
  );
  
  return (
    <input
      onChange={(e) => debouncedSearch(e.target.value)}
      placeholder="Search..."
    />
  );
}
CLS(Cumulative Layout Shift)を最適化する

目標:< 0.1

CLS に関する一般的な問題:

  • 寸法が指定されていない画像
  • スペースが確保されていない広告や埋め込みコンテンツ
  • Web フォントによる FOIT/FOUT
  • 動的なコンテンツの挿入

最適化:

// Always specify image dimensions
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
/>

// Reserve space for dynamic content
<div style={{ minHeight: '200px' }}>
  <Suspense fallback={<Skeleton height={200} />}>
    <DynamicContent />
  </Suspense>
</div>

// Use font-display for web fonts
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap; // Prevent invisible text
}

監査レポート

影響度、工数、優先度を含めて調査結果をまとめます。

レポートのテンプレート
# Performance Audit Report

## Executive Summary
- **Date:** 2026-02-16
- **Auditor:** Engineer Agent
- **Scope:** Full application performance review
- **Overall Score:** 6.5/10

## Critical Issues (Fix Immediately)

### 1. N+1 Query in Post Listing
- **File:** `src/app/posts/page.tsx`
- **Issue:** Sequential database queries for each post author
- **Impact:** Page load time 3.2s -> should be <500ms
- **Effort:** 15 minutes
- **Fix:** Add `include: { author: true }` to findMany query

## High Priority (Fix This Week)

### 2. Missing Bundle Splitting
- **Files:** `src/app/admin/*`
- **Issue:** Admin panel code (250KB) loaded on all pages
- **Impact:** Initial bundle size 850KB -> should be <200KB
- **Effort:** 1 hour
- **Fix:** Use `next/dynamic` for admin routes

## Medium Priority (Fix This Month)

### 3. Unoptimized Images
- **Files:** Multiple components using `<img>` tags
- **Issue:** LCP of 4.1s due to large unoptimized images
- **Impact:** Poor Core Web Vitals, SEO penalty
- **Effort:** 2 hours
- **Fix:** Migrate to `next/image` with proper sizing

## Low Priority (Backlog)

### 4. Missing Memoization
- **Files:** `src/components/Dashboard/*.tsx`
- **Issue:** Unnecessary re-renders on state changes
- **Impact:** Minor UI lag on interactions
- **Effort:** 3 hours
- **Fix:** Add `useMemo`, `useCallback`, `React.memo`

## Performance Metrics

| Metric | Current | Target | Status |
|--------|---------|--------|--------|
| LCP | 4.1s | <2.5s | FAIL |
| INP | 180ms | <200ms | PASS |
| CLS | 0.05 | <0.1 | PASS |
| Bundle Size | 850KB | <200KB | FAIL |
| API Response | 320ms | <500ms | PASS |

検証スクリプト

検証スクリプトを使用して、監査が完了していることを確認します。

bash scripts/validate-performance-audit.sh

スクリプトでは以下を確認します:

  • バンドル分析の成果物
  • データベースクエリのパターン
  • メモ化の使用状況
  • 画像の最適化
  • キャッシュヘッダー

参考資料

アンチパターンと最適化手法の詳細については、references/performance-patterns.md を参照してください。

コメント