目次

  1. 基本原則
  2. 計測とプロファイリング
  3. フロントエンド
  4. バックエンド / アプリケーション
  5. データベース
  6. キャッシュ戦略
  7. ネットワーク / インフラ
  8. アルゴリズムとデータ構造
  9. メモリ管理
  10. 並行処理・非同期処理
  11. 継続的な運用
  12. チェックリスト

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
DBEXPLAIN / EXPLAIN ANALYZE、スロークエリログ
負荷試験k6、JMeter、Locust、wrk

進め方

  1. 本番に近い環境・データ量で計測する
  2. フレームグラフなどでホットスポットを特定する
  3. 1回に1つの変更だけを加えて再計測する
  4. 結果を記録し、チームで共有する

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と無効化ルールが決まっている

運用

  • 本番のパフォーマンスを監視している
  • リグレッションを検出する仕組みがある
  • 変更前後の効果を記録した

0件のページ