audit-performance について
開発者やコーディングエージェント向けのパフォーマンス監査スキルです。実測でボトルネックを特定してから修正し、再測定で効果を確認します。
主な機能
- 01実環境とラボの性能測定
- 02Core Web Vitalsとp95の確認
- 03フロントエンドとDBの問題特定
- 04対象を絞った修正と再測定
- 05Sentry、Lighthouse、EXPLAINの利用
活用シーン
- 01LCP、INP、CLSの問題調査
- 02遅いエンドポイントやN+1の追跡
- 03バンドル、画像、読み込みの点検
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kensaurus/ skills --skill audit-performanceインストール後、Claude Code で/audit-performanceを入力して読み込まれたか確認します。
同梱ファイルreferences/loading-priority-2026.md
詳細
パフォーマンス監査スキル
自由度:MIXED — 最適化する項目は自由に選択 [HIGH freedom];
Sentry/Web Vitals と EXPLAIN による測定を優先 [LOW freedom — run exactly]。
監査・修正の例外。 まず測定し、それから最適化します。JS のペイロード →
audit-bundle-size。瞬時のナビゲーション →enhance-web-instant-nav。限界点 →test-load。タイムアウト/再試行 →audit-resilience。 修正する場合:測定されたボトルネックだけを変更し、再測定します。気づいたほかの遅い処理経路は修正せず、後続対応として記録してください。既存ファイルを書き換えるのではなく、その場で編集してください。パフォーマンステストを追加するのは、リポジトリで既に管理している場合、またはタスクで求められている場合に限ります。リポジトリに含めない一時的な測定を行ってください。
推論の進め方
- 観察 — LCP/INP/CLS、p95、または
EXPLAIN/N+1 ループを引用します(file:line)。 - 解釈 — ユーザーを待たせているのはバイト列、クエリ、それともメインスレッドの処理ですか?
- 分類 — vital-miss/N+1/インデックス不足/バンドル/画像/再レンダリング
- 深刻度 — ライブの処理経路で LCP/INP が不良、または p95 が目標値を大幅に超えている場合は Critical です。
例
観察: Sentry の
/usersの p95 は 2.3s。ハンドラーはfindAllの後、ユーザーごとにOrder.findByUserIdを呼び出しています(app/api/users/route.ts:31)。 解釈: N+1。該当ページの TTFB/LCP に悪影響があります。 分類: N+1。 深刻度: Critical(p95 が 200ms を大幅に超過)。 検出事項:/users| N+1 | 注文を先読みする | その後、再測定
ステップ 0:パフォーマンスデータを収集する [LOW freedom — run exactly]
最適化の前に測定します。複数のソースからデータを収集してください。
本番データ(Sentry)
Sentry のパフォーマンスモニタリングが有効な場合は、本番環境の実測メトリクスを取得します。
sentry:search_events
{
"organizationSlug": "<ORG_SLUG>",
"projectSlug": "<PROJECT_SLUG>",
"regionUrl": "<REGION_URL>",
"query": "slowest transactions by p95 duration in last 7 days",
"limit": 20
}
sentry:search_events
{
"organizationSlug": "<ORG_SLUG>",
"projectSlug": "<PROJECT_SLUG>",
"regionUrl": "<REGION_URL>",
"query": "web vitals LCP INP CLS performance scores in last 7 days",
"limit": 20
}
パフォーマンス関連の問題を確認します。
sentry:search_issues
{
"organizationSlug": "<ORG_SLUG>",
"projectSlugOrId": "<PROJECT_SLUG>",
"regionUrl": "<REGION_URL>",
"query": "performance issues slow queries N+1 in last 30 days",
"limit": 20
}
現在のベンチマークを調査する
最新のパフォーマンス目標を取得します。
firecrawl:firecrawl_search
{
"query": "web vitals thresholds good score <current year>",
"limit": 3,
"sources": [{ "type": "web" }]
}
フィールドデータ(信頼できる情報源) [LOW freedom — run exactly]
ラボデータ(Lighthouse)は診断用です。合否を決めるのはフィールドデータです。
- トラフィック上位のオリジンと URL 5 件について、CrUX/PageSpeed Insights のフィールドデータタブを確認し、p75 の LCP/INP/CLS を記録します(まずモバイルを確認)。
- アプリに
web-vitals/attributionを導入します(ベースビルドではありません)。これにより、LCP 要素、INP を発生させる操作の対象、CLS を引き起こすノードが特定できます。 budget.jsonを使って Lighthouse CI を実行します(references/loading-priority-2026.md§Budgets を参照)。絶対スコアではなく、リグレッションがあれば PR を失敗させます。
コードに手を加える前に、レポートの「Production Metrics」ブロックに 3 つすべてを記録します。
検証プロトコル [LOW freedom — run exactly]
# Field (source of truth): CrUX / PSI field tab → p75 LCP, INP, CLS
# Lab, repeatable, budgeted:
npx @lhci/cli autorun --config=lighthouserc.json
# Early Hints present?
curl -sv https://example.com/ 2>&1 | grep -E "< HTTP/.* 103"
# Speculation: DevTools → Application → Speculative loads
# bfcache: DevTools → Application → Back/forward cache → Test
# LCP element: web-vitals/attribution → onLCP(({attribution}) => attribution.element)
パフォーマンス目標
Core Web Vitals
| メトリクス | 良好 | 要改善 | 不良 |
|---|---|---|---|
| LCP(最大コンテンツの描画) | <2.5s | 2.5-4s | >4s |
| INP(次の描画までのインタラクション) | <200ms | 200-500ms | >500ms |
| CLS(累積レイアウトシフト) | <0.1 | 0.1-0.25 | >0.25 |
その他の主要メトリクス
- TTFB:フィールドデータの p75 で <800ms(Google のしきい値)。エッジでは <200ms を目指します。
- FCP:<1.8s
- 長時間タスク:クリティカルなインタラクション経路(INP の要因)で 50ms を超えるものがないこと
- LCP リソースの読み込み遅延:<100ms(TTFB から LCP 画像のリクエスト開始までの間隔。通常は「画像を検出できない/プリロードされていない」状態です)
- bfcache:すべてのコンテンツページで利用可能にします(DevTools → Application → Back/forward cache)
フロントエンドのパフォーマンス監査 [HIGH freedom]
バンドルサイズの分析
npm run build -- --analyze # framework-specific
npx source-map-explorer 'dist/**/*.js'
チェックリスト:
- バンドル全体が gzip 圧縮後に <200KB
- gzip 圧縮後に 100KB を超えるチャンクがない
- ツリーシェイキングが機能している(バンドルに未使用のエクスポートがない)
- 重いライブラリを遅延読み込みする(チャート、エディター、マップ)
- moment.js を使用していない(date-fns または dayjs を使用)
- lodash を一括インポートしていない(lodash-es または個別インポートを使用)
コード分割
- ルート単位で分割する(各ページが独自のチャンクを読み込む)
- 重いコンポーネントをコンポーネント単位で分割する(
lazy()/dynamic()) - ファーストビューより下のコンテンツを遅延させる
画像の最適化
- WebP/AVIF 形式を使用する(写真に PNG/JPEG を使用しない)
- 画像サイズを適切にする(モバイルに 4K 画像を配信しない)
- ファーストビューより下の画像に
loading="lazy"を指定する - 画面サイズに応じてレスポンシブな
srcSetを指定する - 画像 CDN を使用する(Cloudinary、imgix、Vercel Image Optimization)
読み込み優先度と投機的読み込み [適用する項目の選択は柔軟に、LCP のルールは厳守]
構文の詳細:references/loading-priority-2026.md。即時ナビゲーションの実装 → enhance-web-instant-nav。
LCP 要素(まず web-vitals/attribution または DevTools → Performance → LCP で特定)
- LCP 画像に
loading="lazy"を指定しない — LCP 画像への lazy 指定は自動的に Critical の問題となります - LCP 画像に
fetchpriority="high"を指定する(Next.js:<Image priority />。これにより preload も挿入されます) - LCP 画像を HTML から検出できるようにする — CSS の
background-imageにしない、クライアント JS から挿入しない、ハイドレーション境界の背後に置かない - LCP 画像がレスポンシブで、フレームワークによってすでに preload されていない場合は、
<link rel="preload" as="image" imagesrcset imagesizes>を指定する - 重要度の低い画像:
loading="lazy" decoding="async"を指定する。<picture>では AVIF → WebP → JPEG のフォールバックを用意する
フォント
-
font-display: swapを指定する(ブランド以外のテキストではoptionalも可)。swap による CLS が発生しないよう、フォールバックのメトリクス(size-adjust、ascent-override)を設定する —next/fontが自動的に設定しますが、実際に使用されていることを確認してください - ファーストビューに必要なウェイトのみを preload する。使用する Unicode 範囲にサブセット化し、セルフホストする
Early Hints(103)
- オリジン/CDN が、画像/フォント CDN への
preconnectと LCP 画像+クリティカル CSS のpreloadを含む103 Early Hintsを送信する。確認方法:curl -sv https://site/ 2>&1 | grep -i "< HTTP/.* 103" - Node:
res.writeEarlyHints({ link: [...] })を使用する。それ以外の場合は CDN で設定する
投機的ルール(次のページへのナビゲーションを高速化)
- サイト内リンクに対する
moderateの eagerness のprefetchドキュメントルールを含む<script type="speculationrules">を配置する -
prerenderは、次に表示される可能性が高いページ(ページネーション、「次の記事」、商品詳細など)のみに使用し、eagerness はmoderate/conservativeにする - 投機的読み込みの対象外:ログアウト、カート/チェックアウト、認証、GET に副作用のあるもの、
expects_no_vary_searchが設定されていない?付き URL -
Sec-Purpose: prefetch;prerenderが存在する場合、サーバーは副作用を無視する。分析/広告はdocument.prerendering+prerenderingchangeで制御する - 完全な実装 →
enhance-web-instant-nav
bfcache
-
unloadリスナーを使用しない(pagehideを使用する)。HTML にCache-Control: no-storeを指定しない。pagehide時に WebSocket や IndexedDB トランザクションを開いたままにしない - DevTools → Application → Back/forward cache → 「Test back/forward cache」で緑色の表示になることを確認する
レンダリング戦略(Next.js 16/React 19)
- デフォルトで Server Components を使用する。インタラクティブな末端部分にのみ
'use client'を指定する - 遅いデータを
<Suspense>でラップしてシェルをストリーミングする。ルート全体をブロックするのではなく、静的なシェル+動的な穴(cacheComponents/'use cache')を使用する - 手動の
memo/useMemo/useCallbackを使う前に React Compiler を有効にする(reactCompiler: true)。手動のメモ化は、Profiler で Compiler が最適化できていないと確認された場合にのみ行う
INP(応答性)
- タップ/クリック/キー入力の処理経路に 50ms を超えるタスクを置かない。
scheduler.yield()(フォールバックはsetTimeout(0))またはstartTransitionで処理を分割する - 入力ハンドラー内で同期的なレイアウト読み取り(
offsetHeight、getBoundingClientRect)を行わない - 画面外にある長いセクションに
content-visibility: auto+contain-intrinsic-sizeを指定する - サードパーティスクリプト:
async/deferを指定する。Next の<Script strategy="lazyOnload">(安全な場合は Partytown 経由で"worker")を使用し、同意後に読み込み、埋め込みにはファサードを使用する(lite-youtube、静的な地図画像からクリック時に iframe を表示)
CLS
- すべての
<img>、<video>、<iframe>、広告枠にwidth/heightまたはaspect-ratioを指定する - 後から表示される UI(バナー、トースト、Cookie バー)はスペースを確保するか、オーバーレイ表示にする — コンテンツを押し下げない
React のパフォーマンス(該当する場合)
- React Compiler を有効化し、手動のメモ化は Profiler の根拠がある場合にのみ行う(「読み込みの優先順位と先読み」を参照)
- 不要な再レンダリングがない(React DevTools Profiler)
- 長いリストを仮想化する(react-window、@tanstack/react-virtual)
- JSX の props 内でオブジェクトや配列をインライン生成しない
- Context プロバイダーの適用範囲を狭くする(ローカル状態のためにアプリ全体をラップしない)
CSS のパフォーマンス
- レイアウトスラッシングがない(読み取りを先に行い、その後に書き込む)
- アニメーションには
transform/opacityを使用する(GPU アクセラレーション) -
@importの連鎖がない(バンドルする) - ファーストビューのコンテンツ用にクリティカル CSS をインライン化する
- フォントをサブセット化してプリロードする(
<link rel="preload">)
バックエンドのパフォーマンス監査 [自由度:高]
データベースクエリ
N+1 の検出:
// BAD: N+1
const users = await User.findAll();
for (const user of users) {
const orders = await Order.findByUserId(user.id);
}
// GOOD: eager loading
const users = await User.findAll({ include: [Order] });
チェックリスト:
- N+1 クエリを解消する(イーガーローディング、結合、バッチ読み込み)
- 頻繁にクエリされる列(WHERE、JOIN、ORDER BY)にインデックスを設定する
- 必要な列のみを SELECT する(
SELECT *を使用しない) - すべてのリストエンドポイントにページネーションを設定する
- コネクションプーリングを設定する
- スロークエリのログ記録を有効にする
インデックスの検証: [自由度:低 — このとおりに実行]
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = '123';
API レスポンスの最適化
- レスポンス圧縮を有効にする(gzip/brotli)
- 適切なキャッシュヘッダー(
Cache-Control、ETag)を設定する - 過剰なデータ取得をしない(クライアントが必要とするものだけを返す)
- リストエンドポイントにページネーションを設定する
- p95 のレスポンスタイムを 200ms 未満にする
キャッシュ戦略
| レイヤー | ツール | TTL | 用途 |
|---|---|---|---|
| ブラウザー | Cache-Control ヘッダー | 変動 | 静的アセット、API レスポンス |
| CDN | Vercel/CloudFront/Cloudflare | 1h-1d | 静的ページ、画像 |
| API | Redis/Memcached | 5m-1h | コストの高いクエリ、計算済みデータ |
| ORM | クエリキャッシュ | 1m-5m | 同一のクエリの繰り返し |
ネットワークの最適化
- 静的アセットに CDN を使用する
- HTTP/2 または HTTP/3 を有効にする
- 重要なオリジンに事前接続する(
<link rel="preconnect">) - サードパーティのドメインに DNS プリフェッチを設定する
- 可能な場合は API 呼び出しをバッチ化する(GraphQL、DataLoader)
調査に基づく最適化 [自由度:高]
特定のパフォーマンスのボトルネックについて、最新の解決策を調査します。
firecrawl:firecrawl_search
{
"query": "<framework> <specific bottleneck> performance optimization <current year>",
"limit": 5,
"sources": [{ "type": "web" }]
}
続いて、最適な結果を詳しく読みます。
firecrawl:firecrawl_scrape
{
"url": "<best-result-url>",
"formats": ["markdown"],
"onlyMainContent": true
}
Context7 経由で公式フレームワークのドキュメントを確認します。
context7:resolve-library-id
{
"libraryName": "<framework>",
"query": "performance optimization"
}
すぐにできる改善(効果が高く、労力が少ないもの)
| 問題 | 解決策 | 効果 |
|---|---|---|
| LCP 画像が遅延読み込み、または優先されていない | fetchpriority="high" とプリロードを使用し、LCP 画像から loading="lazy" を削除する | 高(LCP が 300〜800 ms 短縮) |
| 1 ページ目は速いが、2 ページ目は遅い | Speculation Rules の prefetch(中程度) | 高(次のナビゲーションの TTFB ≈ 0) |
| どこでも手動でメモ化している | React Compiler を有効にする | 高(INP。冗長なメモ化を削除) |
| バンドルが大きい | ルートをコード分割し、重いライブラリを遅延読み込みする | 高 |
| 画像の読み込みが遅い | WebP + 遅延読み込み + レスポンシブ対応 | 高 |
| キャッシュがない | Cache-Control ヘッダーを追加する | 高 |
| N+1 クエリ | イーガーロード/バッチ処理 | 高 |
| インデックスが不足している | データベースのインデックスを追加する | 高 |
| フォントが最適化されていない | サブセット化 + プリロード + font-display:swap | 中 |
| 圧縮がない | gzip/brotli を有効にする | 中 |
| コストの高い再レンダリング | まず React Compiler を使用し、手動のメモ化は Profiler の根拠がある場合にのみ行う | 中 |
| レイアウトシフト | 画像/埋め込みコンテンツに幅と高さを明示する | 中 |
修正を適用する前の自己批評 [自由度:低 — 省略禁止]
- 証拠がある — 「遅そうに見える」ではなく、測定済みのバイタル、p95、または
EXPLAINに基づいている - 再現可能 — 同じパスで、対象テーブルのしきい値を引き続き超えている
- 深刻度に根拠がある — Critical は、バイタルの不良または本番パスの p95 に該当する
- 担当が適切 — JS ペイロード →
audit-bundle-size、限界点 →test-load、タイムアウト →audit-resilience - 誤った安心感を与えない — 再レンダリングを測定せずに
memo/useMemoを使わない。修正後に再度測定する - LCP の要素を特定する — 画像の修正を推奨する前に、どの要素が LCP なのかを証明する(アトリビューションデータがなければ、監査ではなく推測)
出力:パフォーマンス監査レポート
## Performance Audit: [Project Name]
### Production Metrics (from Sentry)
- LCP: [value] — [good/needs work/poor]
- INP: [value] — [good/needs work/poor]
- CLS: [value] — [good/needs work/poor]
- Slowest transactions: [list with p95 times]
### Critical Issues (fix immediately)
| # | Area | Issue | Impact | Fix |
|---|------|-------|--------|-----|
| 1 | DB | N+1 on /users endpoint | p95 = 2.3s | Eager load orders relation |
### Optimizations (prioritized)
| # | Area | Issue | Impact | Effort | Fix |
|---|------|-------|--------|--------|-----|
| 1 | FE | Bundle 450KB gzipped | High | Low | Code split routes |
### Already Optimized
- [list of performance areas that are well-implemented]
### Monitoring Recommendations
- [what to add to track performance over time]
### Research Sources
- [URL] — [what optimization pattern it provided]