performance-audit について
performance-audit は、Web アプリの性能を調べる開発者向けスキルです。バンドル、データベース、レンダリング、ネットワーク、メモリを段階的に確認します。
主な機能
- 01段階的な Web 性能監査
- 02バンドルと依存関係の分析
- 03データベースクエリとインデックスの確認
- 04レンダリングと再描画の調査
- 05ネットワークとキャッシュの確認
活用シーン
- 01デプロイ前の性能レビュー
- 02ページ表示の遅さの調査
- 03大規模機能追加後の性能確認
- 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 を参照してください。