performance-profiling について
最適化の前に計測を行う、開発者向けのパフォーマンス分析スキルです。Node.js、React、ブラウザー、データベースクエリを対象に、結果の読み取りとボトルネックの分類を扱います。
主な機能
- 01Node.js の CPU・メモリ分析
- 02フレームグラフの分析
- 03React のレンダリング分析
- 04ブラウザーパフォーマンス診断
- 05データベースクエリ分析
- 06ボトルネック分類のガイダンス
活用シーン
- 01Node.js アプリの CPU ホットパス特定
- 02ヒープ増加やメモリリークの調査
- 03不要な React 再レンダリングの特定
- 04遅いデータベースクエリの分析
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill performance-profilingインストール後、Claude Code で/performance-profilingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.claude-plugin/plugin.jsonreferences/browser-profiling.mdnode-profiling.md詳細
パフォーマンスのプロファイリング
コードに手を加える前に、アプリケーションが実際にどこで時間を費やしているかを特定しましょう。Node.js の CPU・メモリプロファイリング、ブラウザーのフレームグラフ、React のレンダリングプロファイリング、データベースのクエリ分析まで、フルスタックを網羅します。ここでの原則は、まずプロファイルし、その後に最適化することです。早すぎる最適化はワークフローではなく、当てずっぽうです。
使用する場面
次の場合に使用します:
- Node.js アプリケーションの遅さを診断する(CPU バウンド、I/O バウンド、メモリ圧迫)
- ホットコードパスを見つけるためにフレームグラフを生成して読み取る
- ヒープスナップショットと増加傾向からメモリーリークを検出する
- React Profiler で React コンポーネントのレンダリングパフォーマンスをプロファイリングする
- ブラウザーのレンダリングパフォーマンスを測定する(Core Web Vitals、レイアウトスラッシング、長時間タスク)
- EXPLAIN ANALYZE でデータベースクエリをプロファイリングする
- イベントループの利用率とレイテンシを測定する
次の場合には使用しません:
- インフラストラクチャの監視、分散トレーシング、ログ集約(
logging-observabilityを使用してください) - 負荷テストとキャパシティプランニング(別の領域です)
- サービス間のネットワークレイテンシ分析(分散トレーシングツールを使用してください)
- データベーススキーマ設計の最適化(クエリプロファイリングとは別です)
最初に判断すること:アプリの遅い箇所はどこか?
flowchart TD
Start[App is slow. Where?] --> Layer{Which layer?}
Layer -->|Backend| Backend{What kind?}
Layer -->|Frontend/browser| Browser{What symptom?}
Layer -->|Unknown| Measure[Instrument first — add timing logs]
Backend -->|CPU pegged, slow responses| CPU[CPU Profiling]
Backend -->|Memory growing, crashes| Mem[Memory / Heap Profiling]
Backend -->|Fast CPU, slow I/O| IO{I/O type?}
IO -->|Database queries| DB[EXPLAIN ANALYZE + query profiler]
IO -->|Network calls| Network[Trace external calls, add timeouts]
IO -->|File system| FS[Check event loop utilization]
Browser -->|Slow initial load| Lighthouse[Lighthouse + bundle analysis]
Browser -->|Janky scrolling, animations| Rendering[Chrome Performance tab — layout thrashing]
Browser -->|Slow after interaction| React{React app?}
React -->|Yes| ReactProfiler[React Profiler + why-did-you-render]
React -->|No| JS[Chrome Performance — long tasks, main thread blocking]
CPU --> FlameGraph[Generate flame graph with 0x or clinic flame]
Mem --> HeapSnap[Take heap snapshots before/after suspected leak]
FS --> ELU[clinic bubbles — event loop utilization]
Node.js:CPU プロファイリング
V8 Inspector(組み込み)
# Attach inspector and capture a CPU profile
node --inspect src/index.js
# Or start paused and wait for DevTools
node --inspect-brk src/index.js
次に Chrome で chrome://inspect を開き、対象をクリックして Profiler タブに移動し、サーバーに負荷を送信しながら記録します。
0x:ターミナルからフレームグラフを作成
npm install -g 0x
# Profile a script (runs it, generates flame graph)
0x -- node src/index.js
# Profile with a load generator running simultaneously
0x -- node src/server.js &
npx autocannon -d 30 http://localhost:3000/api/heavy
0x はインタラクティブな HTML フレームグラフを生成します。最も幅の広いスタックが時間を費やしている箇所です。次の点を確認します:
- 下部付近で幅が広く表示される関数(あらゆる処理から頻繁に呼び出されています)
- ライブラリーコード内で予想外に幅が広い部分(シリアライズ、テンプレートエンジン、パーサー)
- アイドル状態のブロックや
[idle]ブロック(I/O 待ちであり、CPU 処理ではありません。これらは別の場所を調べてください)
Clinic.js スイート
npm install -g clinic
# Doctor: overview of what is wrong
clinic doctor -- node src/server.js
# Flame: CPU flame graph (wraps 0x)
clinic flame -- node src/server.js
# Bubbles: event loop utilization
clinic bubbles -- node src/server.js
Clinic Doctor は、CPU、メモリ、イベントループ、ハンドルのトリアージビューを表示します。ボトルネックの種類がわからない場合は、ここから始めてください。
イベントループ利用率(ELU)
const { performance } = require('perf_hooks');
// Sample ELU every 5 seconds
let last = performance.eventLoopUtilization();
setInterval(() => {
const current = performance.eventLoopUtilization();
const diff = performance.eventLoopUtilization(current, last);
console.log(`ELU: ${(diff.utilization * 100).toFixed(1)}%`);
last = current;
}, 5000);
ELU が 80% を超える場合、イベントループは飽和状態です。CPU バウンドの処理か、同期処理によるブロックが原因です。応答が遅いのに ELU が 0% 近くの場合は、I/O 待ち(ネットワーク、ディスク、データベース)が原因です。
Node.js:メモリプロファイリング
ヒープスナップショット
# Take heap snapshot via CLI
node --inspect src/index.js
# In chrome://inspect → Memory tab → Take Heap Snapshot
リーク検出のための3スナップショット手法:
- 起動後にスナップショットを取得する(ベースライン)
- N 件のリクエスト後にスナップショットを取得する(ウォーム状態)
- 2N 件のリクエスト後にスナップショットを取得する(増加後)
スナップショット3とスナップショット2を比較します。リクエスト数に比例して増加したオブジェクトは、メモリーリークを起こしています。
よくあるリークパターン
クロージャによるキャプチャ — 解放されるべき変数が、長期間存続するクロージャにキャプチャされている状態です:
// LEAK: handler is registered but never removed
emitter.on('data', (chunk) => {
processedData.push(chunk); // processedData grows unbounded
});
// FIX: remove listener when done, or use once()
emitter.once('data', handler);
// or
const handler = (chunk) => { ... };
emitter.on('data', handler);
// later:
emitter.off('data', handler);
削除処理のないキャッシュの増加:
// LEAK: cache grows forever
const cache = new Map();
app.get('/user/:id', async (req, res) => {
if (!cache.has(req.params.id)) {
cache.set(req.params.id, await db.getUser(req.params.id));
}
res.json(cache.get(req.params.id));
});
// FIX: use LRU cache with max size
const LRU = require('lru-cache');
const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 });
WeakRef と FinalizationRegistry(意図的に弱参照を使用する場合):
const cache = new Map();
function cacheValue(key, obj) {
const ref = new WeakRef(obj);
const registry = new FinalizationRegistry((k) => cache.delete(k));
registry.register(obj, key);
cache.set(key, ref);
}
アンチパターン:プロファイリングせずに最適化する
初心者:「この関数はコストが高そうなので、もっと効率的なアルゴリズムに書き換えます。」
熟練者:間違った関数を書き換えました。プロファイリングしていれば、この関数は起動時に一度しか呼び出されず、実行時間の 0.1% を占めるにすぎないことがわかったはずです。実際のボトルネックはレスポンスハンドラーでの JSON シリアライズで、1 秒あたり 10,000 回呼び出されていました。最適化の取り組みは、必ず測定結果に基づいて進め、直感に頼ってはいけません。
検出方法:「最適化」したコードはマイクロベンチマークの単独実行では測定可能なほど高速になっているのに、本番環境の p99 レイテンシは変わっていません。
アンチパターン:単独でのマイクロベンチマーク
初心者:1000 個の要素を含む配列で 2 つのソートアルゴリズムを比較するベンチマークを書き、アルゴリズム B が 2 倍速いと結論づけて、本番コードを書き換えます。
エキスパート:マイクロベンチマークでは、人工的な条件下で JIT コンパイルされたホットパスを測定します。実際のワークロードでは、データの形状が異なり、さまざまな呼び出しパターンが混在し、GC の負荷がかかり、I/O も途中に挟まります。JIT は、実際の呼び出し箇所とは異なる形でベンチマークを最適化する可能性があります。実際の負荷をかけてアプリケーションそのものをプロファイルするか、少なくとも、実際のアプリケーションのコードパスに組み込んだ、現実的なデータ形状と呼び出しパターンでプロファイルしてください。
テスト:測定前に、ベンチマークをタイトなループで 10,000 回実行していますか? そうであれば、V8 は実際のコードとは異なる形でベンチマークを JIT コンパイルしています。実際のコードは起動時にはコールドで、さまざまな入力を受け取って呼び出されます。
React のレンダリングパフォーマンス
React Profiler(DevTools)
- React DevTools を開き、Profiler タブを選択します
- 「Record」をクリックします
- 遅い操作を実行します
- 録画を停止します
- フレームチャートを確認します。バーはコンポーネントを表し、幅はレンダリング時間を表します
主な列:「Why did this render?」には、各レンダリングのきっかけとなった props または state の変更が表示されます。
why-did-you-render
npm install @welldone-software/why-did-you-render
// src/wdyr.js (import before React)
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, { trackAllPureComponents: true });
}
// Mark a specific component for tracking
MyExpensiveComponent.whyDidYouRender = true;
コンポーネントが同じ props で再レンダリングされるたびに、コンソールにログが出力されます。これにより、参照等価性の失敗が原因で発生する不要なレンダリングを特定できます。
よくある React のパフォーマンスパターン
// Memoize expensive components
const ExpensiveList = React.memo(({ items, onSelect }) => {
return items.map(item => <Item key={item.id} item={item} onSelect={onSelect} />);
});
// Stable callback references — prevent re-renders downstream
const handleSelect = useCallback((id) => {
setSelected(id);
}, []); // no deps: stable forever
// Memoize expensive computations
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// Virtualize long lists
import { FixedSizeList } from 'react-window';
<FixedSizeList height={600} itemCount={items.length} itemSize={50} width="100%">
{({ index, style }) => <Row item={items[index]} style={style} />}
</FixedSizeList>
データベースクエリのプロファイリング
PostgreSQL EXPLAIN ANALYZE
-- Wrap any query in EXPLAIN (ANALYZE, BUFFERS) to see execution plan
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id;
出力は下から上に読みます。各ノードには次の情報が表示されます。
actual time=X..Y— 最初の行が返されるまでの起動時間と、すべての行を返すまでの合計時間rows=N— 実際に返された行数loops=N— このノードが実行された回数
危険信号:
- 大きなテーブルに対する
Seq Scan— インデックスがありません - 推定値が
rows=1000で実際の値がrows=1— 統計情報が古くなっています。ANALYZEを実行してください - ハッシュバッチ数が多い
Hash Join— メモリに負荷がかかっています。work_memを調整してください - 外側の結果が大きい
Nested Loop— 直積が発生するリスクがあります
本番環境で遅いクエリを見つける
-- Enable pg_stat_statements extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Top 10 slowest queries by total time
SELECT
query,
calls,
total_exec_time / 1000 AS total_seconds,
mean_exec_time AS mean_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
ブラウザーのプロファイリング
Chrome Performance タブの全手順、Core Web Vitals の測定、レイアウトスラッシングの診断については、references/browser-profiling.md を参照してください。
ボトルネックの分類ルール
ユーザーからプロファイリングデータが提供されたら、以下のルールを使ってボトルネックを分類し、優先順位を付けてください。シグナルは優先順位の高いものから順に処理します。優先順位の高いシグナルは、低いシグナルに優先します。
優先度 1:データベース(最初に確認 — 70% の確率でボトルネックになります)
| シグナル | 分類 |
|---|---|
| 500ms を超えるクエリがある | 重大 — type: database。次の手順:クエリに対して EXPLAIN ANALYZE を実行します。大きなテーブルへのシーケンシャルスキャン(インデックス不足)と、同じクエリを異なる ID で繰り返す N+1 パターンを探します。 |
| 1 リクエストあたり 100ms を超えるクエリが複数ある | 高 — type: database。次の手順:エンドポイントごとにクエリ数を集計します。1 リクエストあたり 5 件を超える場合は、N+1 または JOIN 不足を確認します。エンドポイントごとのクエリ数の上限設定も検討してください。 |
| ページの読み込みあたりのクエリ数が 20 件を超える | 高 — type: database。個々のクエリが高速でも、接続のオーバーヘッドと往復遅延が積み重なります。次の手順:WHERE id IN (...) でまとめるか、DataLoader パターンを使用します。 |
優先度 2:イベントループ(Node.js 固有 — 最も見落とされやすいボトルネック)
| シグナル | 分類 |
|---|---|
| ELU >0.8 | Critical — type: cpu。イベントループが飽和しています。次のステップ:clinic flame または --prof を実行して、同期的なホットパスを特定します。よくある原因:大きなペイロードに対する JSON.parse、同期的な暗号処理、正規表現のバックトラッキング。 |
| ELU >0.5 かつ p99 レイテンシが遅い | High — type: cpu。イベントループの競合がテールレイテンシを引き起こしています。次のステップ:実行頻度は低くても、実行時にループを占有するブロッキング処理(大規模なソート、テンプレートのレンダリング、PDF 生成)を探します。 |
| ELU <0.2 かつ応答が遅い | これは CPU の問題ではありません。 type: io。次のステップ:アプリは外部の何か(DB、API 呼び出し、ファイルシステム)を待っています。clinic bubbleprof で外向きのリクエストをトレースするか、外部呼び出しにタイミングログを追加します。 |
優先度 3:メモリ
| シグナル | 分類 |
|---|---|
| ヒープ増加率が 10MB/分を超えた状態が継続 | Critical — type: memory。メモリリークによりプロセスが OOM になります。次のステップ:5 分間隔でヒープスナップショットを 2 つ取得し、Chrome DevTools で比較して、増加しているオブジェクト数(保持サイズ)を探します。よくある原因:イベントリスナーの蓄積、リクエストオブジェクトを保持するクロージャー、上限のないキャッシュ。 |
| ヒープ増加がリクエストレートに比例(GC でリセット) | Medium — type: memory。リークではなく、アロケーション負荷が高いだけです。次のステップ:ホットパスで不要なオブジェクト生成(大きなオブジェクトの複製、文字列の連結による構築)がないか確認します。アロケーションを減らし、GC の調査にこだわらないでください。 |
ヒープ分析による suspects 配列 | 各疑い対象とその保持サイズを列挙します。単一のオブジェクトが >50MB を保持している場合は High です。次のステップ:リテーナーツリーをたどり、回収されない原因を特定します。 |
優先度 4:React のレンダリング(フロントエンド)
| シグナル | 分類 |
|---|---|
| コンポーネントのレンダリング時間が >16ms | High — type: rendering。フレームが落ちています。次のステップ:親のレンダリングごとにコンポーネントが再レンダリングされていないか確認します(React.memo がない、または props が不安定)。React DevTools の「Why did this render?」でプロファイリングします。 |
| ユーザー操作ごとに >5 回再レンダリング | Medium — type: rendering。次のステップ:連鎖的な再レンダリングを引き起こす state 更新がないか確認します。state を使用箇所の近くに移動するか、context provider を分割します。 |
| コンポーネントツリーが大きい(マウント済みコンポーネントが >500 個) | Medium — type: rendering。次のステップ:リストを仮想化(react-window)し、画面外のコンポーネントを遅延ロードして、不要なマウント/アンマウントの繰り返しがないか確認します。 |
優先度 5:CPU(イベントループ以外)
| シグナル | 分類 |
|---|---|
CPU プロファイルの selfTime の >30% を単一の関数が占める | High — type: cpu。ホット関数が支配的です。次のステップ:関数を確認します。自分のコードであれば最適化します。ライブラリ内の関数であれば、不要に呼び出していないか、極端に大きな入力を渡していないか確認します。 |
| フレームグラフの幅が広く平坦である(単一のホット関数がない) | Medium — type: cpu。小さな負荷の積み重ねです。次のステップ:パターンを探します。多くの関数が似た処理をしていないでしょうか。これは多くの場合、冗長な計算(リクエストごとに同じ派生値を複数回計算すること)を意味します。 |
出力の順位付け
すべてのシグナルを分類した後、ボトルネックのリストを次の基準で順位付けします。
- 深刻度(Critical を最優先)
- 実行可能性(具体的な次のステップがあるものを、「さらに調査」のような曖昧なものより上位にする)
- 推定される影響 — 「インデックスを追加すれば、このクエリを 800ms から 5ms に短縮できます」は、「改善に役立つ可能性があります」より有用です
estimatedImpact には、必ず具体的な予測を含めてください。「N+1 クエリを解消すれば、リクエスト数が 47 から 3 に減り、エンドポイントのレイテンシが約 200ms 短縮されるはずです」のように記述し、「パフォーマンスが向上するはずです」とは書かないでください。
参考資料
references/node-profiling.md— Node.js の詳細なプロファイリングについては、こちらを参照してください:--inspect フラグ、clinic.js のコマンド、ヒープスナップショットの分析、イベントループの監視、ストリームのバックプレッシャーの診断references/browser-profiling.md— ブラウザーのパフォーマンスについては、こちらを参照してください:Chrome Performance タブのワークフロー、Lighthouse CI の統合、React Profiler の詳細な分析、Core Web Vitals の測定、レイアウトスラッシングのパターン