performance-optimization について
開発者やコーディングエージェント向けのパフォーマンス最適化スキルです。推測ではなく計測に基づく変更を重視し、フロントエンドとバックエンドの診断、再計測、回帰防止を扱います。
主な機能
- 01最適化の前に計測
- 02フロントエンドとバックエンドのボトルネック診断
- 03計測した原因に沿った修正
- 04結果の再計測と回帰の差し戻し
- 05CI と実環境モニタリングによる防止
活用シーン
- 01ページ読み込みや操作の遅さの調査
- 02API 応答やデータベースクエリの遅延追跡
- 03変更による性能回帰の確認
- 04性能予算とモニタリングの設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ addyosmani/ agent-skills --skill performance-optimizationインストール後、Claude Code で/performance-optimizationを入力して読み込まれたか確認します。
同梱ファイルreferences/optimization-patterns.md
詳細
パフォーマンスの最適化
概要
最適化の前に測定しましょう。測定せずにパフォーマンス改善に取り組むのは当てずっぽうです。当てずっぽうでは、重要な部分を改善しないまま複雑さだけを増す、早すぎる最適化につながります。まずプロファイリングを行い、実際のボトルネックを特定して修正し、もう一度測定します。測定によって重要だと確認されたものだけを最適化してください。
使用する場面
- 仕様にパフォーマンス要件(読み込み時間の予算、応答時間の SLA)がある
- ユーザーや監視システムから動作が遅いと報告されている
- Core Web Vitals のスコアがしきい値を下回っている
- 変更によってリグレッションが発生した疑いがある
- 大規模なデータセットや高トラフィックを扱う機能を構築している
使用しない場面: 問題の証拠を得る前に最適化しないでください。早すぎる最適化は、得られるパフォーマンス向上を上回るコストの複雑さを生みます。
Core Web Vitals の目標値
| 指標 | 良好 | 改善が必要 | 不良 |
|---|---|---|---|
| LCP(Largest Contentful Paint) | ≤ 2.5s | ≤ 4.0s | > 4.0s |
| INP(Interaction to Next Paint) | ≤ 200ms | ≤ 500ms | > 500ms |
| CLS(Cumulative Layout Shift) | ≤ 0.1 | ≤ 0.25 | > 0.25 |
最適化のワークフロー
1. MEASURE → Establish baseline with real data
2. IDENTIFY → Find the actual bottleneck (not assumed)
3. FIX → Address the specific bottleneck
4. VERIFY → Measure again; keep or revert
5. GUARD → Add monitoring or tests to prevent regression
ステップ 1:測定する
互いを補完する 2 つの方法があります。両方を使ってください。
- 合成測定(Lighthouse、DevTools の Performance タブ): 条件を制御でき、再現性があります。CI でのリグレッション検出や、特定の問題の切り分けに最適です。
- RUM(web-vitals ライブラリ、CrUX): 実際の条件下での実ユーザーデータです。修正によってユーザーエクスペリエンスが実際に改善したことを検証するために必要です。
フロントエンド:
// Synthetic: Lighthouse in Chrome DevTools (or CI)
// Chrome DevTools → Performance tab → Record
// Chrome DevTools MCP → Performance trace
// RUM: Web Vitals library in code
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
バックエンド:
// Response time logging
// Application Performance Monitoring (APM)
// Database query logging with timing
// Simple timing
console.time('db-query');
const result = await db.query(...);
console.timeEnd('db-query');
測定を始める場所
症状をもとに、最初に測定する項目を決めます。
What is slow?
├── First page load
│ ├── Large bundle? --> Measure bundle size, check code splitting
│ ├── Slow server response? --> Measure TTFB in DevTools Network waterfall
│ │ ├── DNS long? --> Add dns-prefetch / preconnect for known origins
│ │ ├── TCP/TLS long? --> Enable HTTP/2, check edge deployment, keep-alive
│ │ └── Waiting (server) long? --> Profile backend, check queries and caching
│ └── Render-blocking resources? --> Check network waterfall for CSS/JS blocking
├── Interaction feels sluggish
│ ├── UI freezes on click? --> Profile main thread, look for long tasks (>50ms)
│ ├── Form input lag? --> Check re-renders, controlled component overhead
│ └── Animation jank? --> Check layout thrashing, forced reflows
├── Page after navigation
│ ├── Data loading? --> Measure API response times, check for waterfalls
│ └── Client rendering? --> Profile component render time, check for N+1 fetches
└── Backend / API
├── Single endpoint slow? --> Profile database queries, check indexes
├── All endpoints slow? --> Check connection pool, memory, CPU
└── Intermittent slowness? --> Check for lock contention, GC pauses, external deps
ステップ 2:ボトルネックを特定する
カテゴリ別によくあるボトルネック:
フロントエンド:
| 症状 | 考えられる原因 | 調査方法 |
|---|---|---|
| LCP が遅い | 大きな画像、レンダリングをブロックするリソース、遅いサーバー | ネットワークウォーターフォールと画像サイズを確認する |
| CLS が高い | 寸法が指定されていない画像、読み込みが遅れて表示されるコンテンツ、フォントによるレイアウトのずれ | レイアウトシフトの原因を確認する |
| INP が低い | メインスレッド上の負荷の高い JavaScript、大規模な DOM 更新 | Performance トレースで長時間タスクを確認する |
| 初回読み込みが遅い | 大きなバンドル、多数のネットワークリクエスト | バンドルサイズとコード分割を確認する |
バックエンド:
| 症状 | 考えられる原因 | 調査方法 |
|---|---|---|
| API の応答が遅い | N+1 クエリ、インデックスの不足、最適化されていないクエリ | データベースのクエリログを確認する |
| メモリ使用量の増加 | 参照のリーク、上限のないキャッシュ、大きなペイロード | ヒープスナップショットを分析する |
| CPU 使用率の急上昇 | 同期的な高負荷計算、正規表現のバックトラッキング | CPU プロファイリングを行う |
| レイテンシが高い | キャッシュの不足、冗長な計算、ネットワークのホップ | スタック全体でリクエストをトレースする |
ステップ 3:ボトルネックを修正する
ステップ 2 で特定したことだけを修正し、それ以外には手を加えないでください。以下のアンチパターンはよく見られる原因です。それぞれの項目に、ルールと見分けるための特徴が記載されており、修正例は references/optimization-patterns.md にあります。コードに到達したときに必要な項目だけを開き、事前に読む必要はありません。
バックエンド
- N+1 クエリ。 1 行ごとにクエリを実行することは、バックエンドで最もよくあるボトルネックです。ループ内で取得するのではなく、同じクエリでリレーションを取得します(join/include)。パターン。
- 上限のないデータ取得。 すべてのリストエンドポイントで、上限と安定した順序を指定してページネーションを行います。パターン。
- インデックスが使われないクエリ。 「インデックスを追加する」は推測ですが、
EXPLAIN ANALYZEは実測です。インデックスが使われるはずの箇所にSeq Scanがある場合、rows=の推定値が桁違いの場合、スキャンの上にSortノードがある場合では、それぞれ必要な修正が異なります。rows=の推定値が不正確なら統計情報が古いため、インデックスを追加するのではなくANALYZEを実行します。クエリの形に合わせてインデックスを作成します(等価条件の列を先にし、その後に範囲またはソートの列を指定します)。通常のインデックスでは、選択性の低い値が支配的なクエリ(まれな値には部分インデックスを使用)、先頭にワイルドカードがあるクエリ(トライグラムまたは全文検索が必要)、列に関数を適用するクエリ(WHERE lower(email) = ?のように式にインデックスを作成)には効果がありません。また、インデックスを追加すると、書き込みのたびに負荷がかかります。その後、実行計画を再確認します。実行計画が変わらなかったインデックスは元に戻します。パターン。 - コネクションプールの枯渇。 典型的な兆候は、すべてのエンドポイントが一斉に遅くなり、実行中ではなくコネクション待ちに時間がかかり、データベースのセッションの大半がアイドル状態になっていることです。プロセスごとにプールを 1 つ用意し、
instances × maxがデータベースのコネクション上限を下回るようにサイズを設定します。プールを大きくしても速くなるとは限りません。キューがデータベース側に移り、見えにくくなるだけです。インスタンス数に上限がない場合(サーバーレス、自動スケーリング)は、maxを増やすのではなく、プロキシ(pgbouncer、RDS Proxy)経由で多重化します。パターン。 - キャッシュの不足。 生成コストが高く、変更頻度よりも読み取り頻度がはるかに高いデータをキャッシュします。すでに高速なクエリをキャッシュすると、何も得られないままネットワークホップが増え、古いデータを返す不具合が生じます。レイヤー(プロセス内、共有、CDN)を意図的に選びます。レスポンスが変わる要素はすべてキーに含めます(テナント、ロケール、権限、フィーチャーフラグ)。閲覧者を含めないキーでは、あるユーザーのデータが別のユーザーに返される可能性があります。無効化戦略(TTL、イベントまたはタグベース、バージョン付きキー)を 1 つ選び、許容するデータの古さの期間を明示します。ホットキーへの集中アクセスには対策を講じます。1 つのリクエストが再計算する間は古いデータを返すか、処理中の単一の Promise に同時に発生したキャッシュミスを集約します。最新である必要があるデータ(残高、権限、チェックアウト時の在庫)は決してキャッシュしません。パターン。リクエストの集約、書き込み戦略、ネガティブキャッシュについては
../../references/performance-checklist.mdを参照してください。
フロントエンド
- 画像最適化の不足。 すべての画像で
widthとheightを指定します(CLS)。LCP 画像にはfetchpriority="high"を設定し、<picture>で最新の形式(AVIF、WebP)を使用して、解像度の切り替えにはsrcset/sizesを指定します。画面下部の画像にはloading="lazy"とdecoding="async"を設定します。パターン。 - 不要な再レンダリング。 レンダリング中に作成されたオブジェクトや関数は、毎回新しい参照となり、それを受け取るすべての子コンポーネントを再レンダリングします。定数はレンダリングの外に定義します。
React.memoとuseMemoは、プロファイルで処理コストが高いと確認できた場合に限って使用します。過剰な使用にもコストがかかります。パターン。 - バンドルサイズが大きい。 最新のバンドラーは、ESM の名前付きインポートを自動でツリーシェイクします。実際に効果があるのは、ルート単位のコード分割と、
Suspense配下での使用頻度が低く容量の大きい機能の遅延読み込みです。インポート形式を変える前にプロファイルを行います。パターン。
ステップ 4:検証する(維持するか、差し戻すか)
再測定するまでは、修正は仮説にすぎません。このステップで、その修正を維持するかどうかを判断します。
**ベースラインと同じ方法で再測定します。**同じコマンド、同じ条件、同じ固定予算(実時間、サンプル数、またはリクエスト数)を使います。コールドキャッシュで取得したベースラインと、ウォームキャッシュで取得した結果を比べると、測定しているのは変更ではなくキャッシュです。
**一度に変更するのは1つだけにします。**最適化を3つまとめて適用しても、得られる数値は1つだけなので、どの変更の効果か特定できません。まとめてリリースする必要がある場合は、まずそれぞれを個別に測定します。
**平均値だけでなく、ノイズを上回る結果を目指します。**測定を繰り返し、差分を実行ごとのばらつきと比較します。±5%のばらつきの範囲内にある3%の改善は改善ではなく、異なるサンプルにすぎません。
そのうえで、厳密に判断します。
| ベースラインとの比較結果 | 対応 |
|---|---|
| しきい値を超え、テストも成功 | **維持。**メッセージに変更前と変更後の数値を記載してコミットします。 |
| ノイズの範囲内(測定可能な変化なし) | 差し戻します。 |
| 悪化 | 差し戻します。 |
| 改善したが、テストが失敗 | **差し戻します。**成功に見せかけたリグレッションです。 |
**「変化なし」は維持ではなく、差し戻しです。**ここがチームが見落としがちな点です。変更をすでに書いてしまうと、破棄するのはもったいなく感じるため、測定されないまま取り込まれ、コードベースには何の効果もなかった複雑さが蓄積していきます。維持するコードは、ずっと保守し続けることになります。労力に見合う成果を出させましょう。
**正しさは指標より優先です。**テストスイートが成功し、かつ数値が改善している必要があります。製品に必要な処理を省くことで改善している「最適化」(検証をスキップする、最新である必要があるものをキャッシュする、重要な await を削除するなど)は、成功ではなくリグレッションです。
差し戻したものも含め、試したことをすべて記録する
差し戻した作業は Git の履歴に残りません。そのため、同じ効果のないアイデアが次の四半期にまた試されてしまいます。破棄したアイデアが再び持ち出されないよう、簡潔な記録を残しましょう。
| アイデア | ベースライン → 結果 | 判定 | 理由 |
|---|---|---|---|
| 行コンポーネントをメモ化する | INP 240ms → 235ms | 差し戻し | ノイズの範囲内(±15ms)です。ボトルネックは行ではありませんでした。 |
| リストを仮想化する | INP 240ms → 90ms | 維持 | トレースから長時間タスクがなくなりました。 |
| API オリジンに事前接続する | LCP 2.8s → 2.8s | 差し戻し | すでに同一オリジンでした。 |
PR の説明にセクションを追加する方法でも、リポジトリに PERF.md を置く方法でも構いません。大切なのは、次の担当者(または次のエージェント)が実験を提案する前にその記録を読み、すでに失敗した実験を繰り返さないことです。
ステップ 5:リグレッションを防ぐ
ユーザーが実際に体感する指標を監視しましょう。取得できるすべての数値を監視する必要はありません。修正の根拠となった同じ LCP、INP、p95 レイテンシ、またはその他の主要指標を使います。
ユーザー向けの機能では、次の2つの補完的なレイヤーを使います。
- **合成テストの CI ゲート:**パフォーマンス予算を設け、マージ前に再現性のあるリグレッションを検出します。ノイズの多い測定は繰り返すか、中央値や傾向を比較して、通常の実行ごとのばらつきによってゲートが不安定にならないようにします。
- **実環境でのモニタリング:**RUM データで p75 に意味のある変動があればアラートを出します。原因の特定には、要因が特定された
web-vitalsデータを使います。CrUX のローリングウィンドウは、即時アラートではなく確認用として扱います。
いずれかのガードが作動したら、ステップ 1 に戻り、新たな修正を提案する前に新しいベースラインを確立します。
予算を設定し、適用します。
JavaScript bundle: < 200KB gzipped (initial load)
CSS: < 50KB gzipped
Images: < 200KB per image (above the fold)
Fonts: < 100KB total
API response time: < 200ms (p95)
Time to Interactive: < 3.5s on 4G
Lighthouse Performance score: ≥ 90
CI で適用します。
# Bundle size check
npx bundlesize --config bundlesize.config.json
# Lighthouse CI
npx lhci autorun
関連項目
パフォーマンスの詳細なチェックリスト、最適化コマンド、アンチパターンの一覧については、../../references/performance-checklist.md を参照してください。
よくある合理化
| 合理化 | 実際 |
|---|---|
| 「後で最適化します」 | パフォーマンス上の負債は積み重なります。明らかなアンチパターンは今すぐ修正し、細かな最適化は後回しにしましょう。 |
| 「自分のマシンでは速いです」 | あなたのマシンはユーザーのものではありません。代表的なハードウェアとネットワークでプロファイリングしましょう。 |
| 「この最適化は明らかです」 | 計測していないなら、分かっているとは言えません。まずプロファイリングしましょう。 |
| 「100msならユーザーは気付きません」 | 調査によると、100msの遅延はコンバージョン率に影響します。思っている以上にユーザーは気付きます。 |
| 「フレームワークがパフォーマンスを処理してくれます」 | フレームワークは一部の問題を防ぎますが、N+1クエリや肥大化したバンドルは解決できません。 |
| 「クエリが遅いので、インデックスを追加しましょう」 | まず実行計画を確認しましょう。インデックスはすでに存在していて使えない可能性があり、インデックスを追加するたびに書き込みのコストが永久に増えます。 |
| 「とにかくキャッシュしましょう」 | もともと低コストな呼び出しをキャッシュしても何も得られず、データ鮮度に関するバグが増えます。高コストで、読み取りが書き込みを大幅に上回るものをキャッシュしましょう。 |
| 「接続が足りないので、プールサイズを増やしましょう」 | データベースが処理できる量を超えてプールを大きくすると、キューが見えにくい場所へ移るだけです。何が接続を占有しているのかを調べましょう。 |
| 「あまり効果はありませんでしたが、害もありません」 | 効果がない変更は元に戻しましょう。その変更には今後ずっと保守コストがかかり、何も得られませんでした。 |
| 「もう書いてしまったので、残しておけばいいでしょう」 | サンクコストです。変更を書くのにどれだけ時間がかかったかは、計測結果には関係ありません。 |
| 「改善は明らかなので、再計測は不要です」 | それなら再計測は簡単ですし、効果を証明できます。計測されていない改善を信じることで、効果のない複雑さが持ち込まれます。 |
危険信号
- 正当性を示すプロファイリングデータなしで最適化している
- データ取得にN+1クエリのパターンがある
- 根拠となる変更前後のクエリ実行計画なしでインデックスを追加している
- レスポンスが依存する入力(テナント、ロケール、閲覧者など)がキャッシュキーから漏れている
- 許容するデータ鮮度の期間が明示されておらず、無効化戦略もないキャッシュ
- 接続枯渇への対応として、接続を占有しているものを特定せずに接続プールのサイズを増やしている
- ページネーションのないリストエンドポイント
- 寸法指定、遅延読み込み、レスポンシブサイズのない画像
- レビューされないままバンドルサイズが増加している
- 本番環境でパフォーマンスを監視していない
- あらゆる場所で
React.memoとuseMemoを使用している(過剰な使用は、使用不足と同じくらい問題です) - 効果を正当化する再計測なしに最適化を残している
- 1回の計測に複数の最適化をまとめているため、個々の変更の効果を特定できない
- テストの変更、スキップ、削除を必要とした「改善」
- 最初の試みが記録されていないため、失敗した最適化を複数回試している
検証
パフォーマンス関連の変更を行った後は、次の点を確認します。
- 変更前と変更後の計測結果(具体的な数値)がある
- ベースラインと同じ方法(同じコマンド、同じ条件)で結果を再計測した
- 平均値だけでなく、実行ごとのばらつきを上回る改善がある
- ベースラインを上回らなかった変更は、効果なしのまま残さず元に戻した
- 試行は、採用したものも元に戻したものも記録されており、効果のなかったアイデアを再試行しない
- 具体的なボトルネックを特定し、対処した
- Core Web Vitals が「Good」のしきい値内にある
- バンドルサイズが大幅に増加していない
- 新しいデータ取得コードにN+1クエリがない
- 新しいインデックスは、変更前後のクエリ実行計画で正当性が示されており、書き込みコストも考慮されている
- 新しいキャッシュについて、どのキーに基づくか、どのように古くなるかが明示されている
- 計測対象のユーザー向け指標に、回帰を検知できる合成モニターまたは実環境モニターがある
- 既存のテストが引き続き成功する(最適化によって動作が壊れていない)