目次
- 基本原則
- 計測とプロファイリング
- フロントエンド
- バックエンド / アプリケーション
- データベース
- キャッシュ戦略
- ネットワーク / インフラ
- アルゴリズムとデータ構造
- メモリ管理
- 並行処理・非同期処理
- 継続的な運用
- チェックリスト
1. 基本原則
| 原則 | 内容 |
|---|---|
| 計測してから最適化する | 推測で手を入れない。ボトルネックはデータで特定する |
| 目標を数値で決める | 「速くする」ではなく「p95レイテンシ 200ms以下」のように定義する |
| 最大のボトルネックから着手 | アムダールの法則:全体の5%しか占めない処理を2倍速くしても効果は小さい |
| 早すぎる最適化を避ける | まず正しく読みやすいコードを書き、必要な箇所だけ最適化する |
| トレードオフを意識する | 速度・メモリ・可読性・保守性・コストのバランスを取る |
| 変更前後で比較する | ベンチマークで改善を確認し、リグレッションがないか検証する |
「早すぎる最適化は諸悪の根源」— ただし、明らかに非効率な設計(N+1クエリ、O(n²)のループなど)は最初から避けるべき。
2. 計測とプロファイリング
見るべき指標
- レイテンシ:平均ではなく p50 / p95 / p99 のパーセンタイルで見る
- スループット:単位時間あたりの処理件数(RPS / QPS)
- リソース使用率:CPU・メモリ・ディスクI/O・ネットワーク帯域
- エラー率:負荷が上がったときのタイムアウトや失敗
- Web体験指標(Core Web Vitals)
- LCP(Largest Contentful Paint):2.5秒以内
- INP(Interaction to Next Paint):200ms以内
- CLS(Cumulative Layout Shift):0.1以下
主なツール
| 領域 | ツール例 |
|---|---|
| ブラウザ | Chrome DevTools(Performance / Network / Lighthouse)、WebPageTest |
| アプリケーション | 言語別プロファイラ(py-spy、pprof、async-profiler、Node.js --prof) |
| APM / 分散トレース | OpenTelemetry、Datadog、New Relic、Jaeger |
| DB | EXPLAIN / EXPLAIN ANALYZE、スロークエリログ |
| 負荷試験 | k6、JMeter、Locust、wrk |
進め方
- 本番に近い環境・データ量で計測する
- フレームグラフなどでホットスポットを特定する
- 1回に1つの変更だけを加えて再計測する
- 結果を記録し、チームで共有する
3. フロントエンド
3.1 読み込みの最適化
- バンドルサイズを削減
- Tree shaking で未使用コードを除去
- コード分割(Code Splitting)とルート単位の遅延読み込み(
import()) - 重い依存ライブラリを軽量な代替に置き換える(バンドルアナライザで確認)
- 圧縮:Brotli / gzip を有効化、JS・CSSは minify する
- クリティカルパスの短縮
- ファーストビューに必要なCSSをインライン化
<script>にはdefer/asyncを付けるpreload/preconnect/dns-prefetchで重要リソースを先読み
3.2 画像・メディア
- WebP / AVIF などの次世代フォーマットを使う
srcset/sizesで画面に合ったサイズを配信- ファーストビュー外の画像は
loading="lazy" - LCP対象の画像は
fetchpriority="high"を指定し、遅延読み込みしない width/heightを明示してレイアウトシフト(CLS)を防ぐ- 動画のGIFは MP4 / WebM に置き換える
3.3 フォント
font-display: swapで表示ブロックを回避- 必要な文字だけのサブセット化(日本語フォントでは特に効果大)
- WOFF2 を使い、重要なフォントは
preload
3.4 レンダリングと実行
- 長いタスク(50ms超)を分割し、メインスレッドをブロックしない
- 重い計算は Web Worker に逃がす
- レイアウトスラッシング(読み取りと書き込みの交互実行)を避ける
- アニメーションは
transform/opacityを使い、requestAnimationFrameで制御 - 大量のリストは仮想スクロール(windowing)で描画数を抑える
- スクロール・リサイズなどのイベントは debounce / throttle する
3.5 フレームワーク(React等)固有
- 不要な再レンダリングを防ぐ(
memo、useMemo、useCallbackは計測した上で使う) - 状態を必要な範囲に局所化する
- SSR / SSG / ストリーミングを適切に選ぶ
- リストには安定した
keyを付ける
4. バックエンド / アプリケーション
- I/O待ちを減らす:外部API・DB呼び出しの回数をまとめる(バッチ化)
- 独立した処理は並列化:依存のない外部呼び出しは同時に実行する
- 重い処理は非同期化:メール送信・画像変換などはジョブキュー(SQS、RabbitMQ、Sidekiq等)へ
- コネクションプールを使う:DB・HTTPクライアントの接続を使い回す
- タイムアウトとリトライを設定:遅い依存先に引きずられないようにする(サーキットブレーカーも検討)
- ペイロードを小さく:必要なフィールドだけ返す、ページネーションを導入、レスポンスを圧縮
- シリアライズ処理を見直す:大きなJSONの生成・パースはボトルネックになりやすい
- ログ出力を適正化:ホットパスでの過剰なログや同期書き込みを避ける
- 起動時処理と実行時処理を分ける:設定読み込み・正規表現コンパイルなどは一度だけ行う
5. データベース
5.1 クエリ
- N+1問題を解消:JOIN・
IN句・ORMのEager Loading(includes、select_related等)を使う SELECT *を避け、必要なカラムだけ取得するEXPLAINで実行計画を確認し、フルスキャンを見つける- 大量データは
LIMIT/ キーセットページネーション(OFFSETが大きいと遅い) - インデックスが効かない書き方を避ける(カラムへの関数適用、前方一致でない
LIKE '%xxx'など)
5.2 インデックス
WHERE・JOIN・ORDER BYで使うカラムにインデックスを張る- 複合インデックスはカラム順序が重要(選択性と検索条件の順を考慮)
- カバリングインデックスでテーブルアクセスを省く
- インデックスの張りすぎは書き込み性能を落とすため、使われていないものは削除する
5.3 設計・運用
- 書き込みはバルクインサート / バルクアップデートでまとめる
- トランザクションは短く保ち、ロック競合を減らす
- 読み取り負荷はリードレプリカに分散
- 巨大テーブルはパーティショニングを検討
- 正規化を基本とし、読み取りが極端に多い箇所だけ非正規化する
- 統計情報の更新・VACUUM などのメンテナンスを定期実行する
6. キャッシュ戦略
キャッシュの階層
ブラウザキャッシュ → CDN → リバースプロキシ → アプリ内メモリ → 分散キャッシュ(Redis等) → DB
ユーザーに近い層でヒットするほど効果が高い。
ポイント
- HTTPキャッシュ
- 静的アセットはファイル名にハッシュを付け、
Cache-Control: public, max-age=31536000, immutable - HTMLやAPIは
ETag/Last-Modifiedで条件付きリクエスト
- 静的アセットはファイル名にハッシュを付け、
- アプリケーションキャッシュ
- Cache-Aside(読み取り時にキャッシュを確認し、なければDBから取得して格納)が基本
- TTLを必ず設定する
- キャッシュキーの設計を統一する(バージョンを含めると一括無効化しやすい)
- 注意点
- キャッシュの無効化が最も難しい。更新時の整合性ルールを決めておく
- キャッシュスタンピード対策:ロック、TTLにランダム幅(ジッター)、事前更新
- ヒット率を監視し、効いていないキャッシュは見直す
- 個人情報や認証が必要なデータを共有キャッシュに載せない
7. ネットワーク / インフラ
- CDN で静的コンテンツ(必要に応じて動的コンテンツも)をエッジ配信
- HTTP/2・HTTP/3 を有効化し、多重化と接続確立の高速化を活かす
- リクエスト数を削減:API呼び出しの集約(BFF、GraphQL等)
- 地理的配置:ユーザーやDBに近いリージョンにサーバーを置く
- TLS最適化:セッション再開、OCSPステープリング
- 水平スケール:ステートレス設計にしてオートスケールを可能にする
- ロードバランシング:ヘルスチェックとコネクションの偏り対策
- リソースの適正化:インスタンスサイズやコンテナのCPU/メモリ制限を実測に基づいて調整
8. アルゴリズムとデータ構造
- 計算量(Big-O)を意識する。データ量が増えたときに効いてくる
- 用途に合ったデータ構造を選ぶ
| 用途 | 適した構造 |
|---|---|
| 存在確認・重複排除 | ハッシュセット(O(1)) |
| キーによる検索 | ハッシュマップ / 辞書 |
| 順序付き検索・範囲検索 | 平衡二分木、ソート済み配列+二分探索 |
| 優先度順の取り出し | ヒープ(優先度付きキュー) |
| 先頭・末尾の追加削除 | デック(両端キュー) |
- ループ内で不変な計算をループ外へ出す
- 同じ計算を繰り返す場合はメモ化する
- 文字列の連結はループ内で
+を繰り返さず、ビルダーやjoinを使う - 早期リターン・短絡評価で不要な処理を省く
9. メモリ管理
- メモリリークを防ぐ:イベントリスナー・タイマー・グローバル参照・クロージャの解放忘れに注意
- 大きなデータはストリーム処理:ファイルやレスポンスを丸ごとメモリに載せない
- オブジェクト生成を減らす:ホットパスでの一時オブジェクトの大量生成はGC負荷になる(必要に応じてオブジェクトプール)
- 遅延評価:ジェネレータ / イテレータで必要な分だけ処理
- GCの挙動を把握:GCの停止時間がレイテンシのスパイクになっていないか監視
- ヒープスナップショットで増え続けるオブジェクトを特定する
10. 並行処理・非同期処理
- I/Oバウンドな処理は非同期I/O(async/await、イベントループ)を使う
- CPUバウンドな処理はマルチプロセス / ワーカースレッドで並列化
- 非同期コードの中で同期的なブロッキング処理を呼ばない
- 並列数には上限を設ける(セマフォ、ワーカープール)。無制限だと依存先を圧迫する
- ロックの粒度を小さく、保持時間を短くする
- 共有状態を減らし、可能ならイミュータブルなデータやメッセージパッシングを使う
- バックプレッシャーを考慮し、処理しきれない入力を溜め込まない
11. 継続的な運用
- パフォーマンスバジェットを設定する(例:JSバンドル 200KB以下、LCP 2.5秒以内)
- CIにベンチマーク・Lighthouseを組み込み、リグレッションを自動検出
- 本番監視(RUM / APM) でユーザーが実際に体験している速度を把握
- SLO / アラートを設定し、劣化に早く気づけるようにする
- 定期的な負荷試験でキャパシティの限界を把握しておく
- 最適化の内容と効果を記録し、知見として残す
12. チェックリスト
着手前
- 改善目標を数値で定義した
- 現状のベースラインを計測した
- プロファイリングでボトルネックを特定した
フロントエンド
- バンドルサイズを確認し、コード分割している
- 画像は適切なフォーマット・サイズで、遅延読み込みしている
- Core Web Vitals が目標値を満たしている
- 静的アセットに長期キャッシュを設定している
バックエンド / DB
- N+1クエリがない
- 主要なクエリにインデックスが効いている
- 重い処理を非同期ジョブに逃がしている
- コネクションプール・タイムアウトを設定している
- キャッシュのTTLと無効化ルールが決まっている
運用
- 本番のパフォーマンスを監視している
- リグレッションを検出する仕組みがある
- 変更前後の効果を記録した