サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

【現場完全ガイド】Laravelのパフォーマンス最適化――遅い原因の見つけ方、N+1対策、キャッシュ/HTTPキャッシュ、キュー化、DBインデックス、Redis、フロント最適化、アクセシブルな高速体験

php elephant sticker

Photo by RealToughCandy.com on Pexels.com

【現場完全ガイド】Laravelのパフォーマンス最適化――遅い原因の見つけ方、N+1対策、キャッシュ/HTTPキャッシュ、キュー化、DBインデックス、Redis、フロント最適化、アクセシブルな高速体験

この記事で学べること(要点)

  • 速度低下の原因を「計測→仮説→改善→再計測」で潰す実務フロー
  • N+1、不要なSELECT、重い集計、遅い外部API、遅いI/Oなど典型ボトルネックの見分け方
  • Eloquent最適化(with/withCount/select/chunk/cursor)とDBインデックス設計
  • キャッシュ(アプリ/クエリ/ビュー/設定)、Redis、HTTPキャッシュ(ETag/304)
  • キュー化と非同期化(メール/画像/PDF/集計)、Horizon監視
  • フロント/配信の最適化(圧縮、HTTP/2、CDN、画像最適化)
  • アクセシブルな「速い体験」:ローディングの読み上げ、スケルトンの注意点、prefers-reduced-motion、待ち時間の見せ方

想定読者(だれが得をする?)

  • Laravel 初〜中級エンジニア:遅さの原因を再現し、改善を積み上げられるようになりたい方
  • テックリード/運用担当:計測と監視を整え、性能劣化をデプロイ前に止めたい方
  • デザイナー/QA/アクセシビリティ担当:待ち時間のUIを「誰でも分かる」形に整えたい方

アクセシビリティレベル:★★★★★

読み上げで分かるローディング(aria-busy/role="status")、色に依存しない進捗、動きの抑制、キーボードで待ち時間中も迷子にならない導線などを含めます。


1. はじめに:最適化は“勘”ではなく“計測”です

パフォーマンス最適化は、速くすること自体が目的ではなく「ユーザーが待たされない」「サーバ費用を抑える」「障害を減らす」ための手段です。そして、最短で成果を出すには、勘に頼らず 計測→改善→再計測 を繰り返すことが重要です。

Laravelは便利な分、何もしないと遅くなる余地もあります。でも逆に言えば、ボトルネックの型がはっきりしているので、順番に潰せば着実に速くなります。


2. まず計測:どこが遅いかを特定する

2.1 目標(SLO)を先に決める

例:

  • 主要画面の p95 が 300ms 未満
  • APIの p95 が 200ms 未満
  • 5xx率 0.1% 未満

「どの画面が重要か」を決めないと、最適化は終わりません。

2.2 計測ポイントの基本

  • アプリ:リクエスト時間、SQL数、SQL時間、外部HTTP時間、キュー遅延
  • DB:スロークエリ、ロック、インデックスの利用状況
  • インフラ:CPU、メモリ、I/O、ネットワーク、キャッシュヒット率
  • UX:TTFB、LCP、CLS、INP(Web Vitals)

2.3 Laravel側の実務ツール

  • Telescope:開発・検証でリクエスト/SQL/例外を可視化
  • ログ:構造化ログ(trace_id と時間)
  • APM:Sentry/Datadog/New Relic など(導入できるなら強力)

3. 典型ボトルネックの地図(最初に潰す順番)

現場で成果が出やすい順に並べます。

  1. N+1(SQLが増えすぎ)
  2. 必要のないカラム/行を取る(SELECT過多、ページングなし)
  3. 重い集計(COUNT/GroupByが毎回走る)
  4. 外部APIが遅い/不安定
  5. 画像/PDF生成など重い処理を同期実行
  6. キャッシュが無い/効いていない
  7. DBインデックスが不適切
  8. フロント配信(圧縮なし/画像が重い/CDNなし)

4. N+1対策:Eloquentの基本の基本

4.1 N+1の例(悪い)

$posts = Post::latest()->take(20)->get();
foreach ($posts as $post) {
  echo $post->user->name; // ここで20回追加SQLが発生しがち
}

4.2 with で先読み(良い)

$posts = Post::with('user')->latest()->take(20)->get();

4.3 件数も withCount

$posts = Post::withCount('comments')->latest()->paginate(20);

4.4 さらに select で必要最小限

$posts = Post::query()
  ->select(['id','user_id','title','created_at'])
  ->with(['user:id,name'])
  ->latest()
  ->paginate(20);

ポイント

  • 取得する列を減らすとメモリと転送量が減ります。
  • with は万能ではありません。過剰に入れると逆に重くなるので、重要画面から段階的に。

5. DBクエリ最適化:ページング、インデックス、スロークエリ

5.1 ページングは必須

一覧で全件返すと、遅いだけでなくメモリも死にます。
paginate() または simplePaginate() を基本に。

5.2 インデックスの鉄則

  • WHERE/ORDER BY の組み合わせに合わせて複合インデックス
  • tenant_id を使うなら (tenant_id, created_at) のように先頭に含める
  • “よく使う条件”の順に設計する

例:

  • WHERE status = ? AND created_at >= ? ORDER BY created_at DESC
    (status, created_at) を検討

5.3 EXPLAIN の読み方(簡易)

  • type=ALL(全表走査)が出ていたら要注意
  • rows が極端に多いクエリを優先して直す
  • Using filesort は必ずしも悪ではないが、頻出なら見直し対象

6. 大量データ処理:chunk / cursor / lazy

6.1 chunk

User::where('active', true)->chunkById(1000, function($users){
  foreach ($users as $u) { /* 処理 */ }
});

6.2 cursor(省メモリ)

foreach (User::where('active', true)->cursor() as $u) {
  // 1件ずつ、メモリを食いにくい
}

6.3 注意

  • cursorは1件ずつSQLを投げるわけではなく、内部でイテレータとして流しますが、関連読み込みには注意が必要です(N+1が再発しやすい)。
  • 大量処理は基本、キュー化と相性が良いです。

7. キャッシュ:最小の努力で最大の効果を出す

7.1 まずは「高コストで変化が少ない」もの

  • トップページのランキング
  • ナビのカテゴリ一覧
  • ダッシュボードの集計
  • 設定値(Feature flag、プラン制限)
$items = Cache::remember('top:popular', 300, function(){
  return Product::orderByDesc('popularity')->take(20)->get();
});

7.2 キャッシュキーの設計

  • ロケール、テナント、ユーザーに依存するならキーに含める
  • 例:t:{tenant_id}:home:popular:ja

7.3 タグキャッシュ(対応ストアのみ)

更新時にまとめて破棄できるようになるので、運用が楽です。


8. HTTPキャッシュ:ETag/304で帯域削減

更新が少ないAPIや一覧は、条件付きリクエストで効きます。

  • 変更なし → 304
  • 変更あり → 200 + ETag
$etag = sha1($updatedAt.$id);
if (request()->header('If-None-Match') === $etag) {
  return response()->noContent(304)->header('ETag',$etag);
}
return response()->json($data)->header('ETag',$etag);

9. キュー化:重い処理は“待たせない”

9.1 典型の非同期化対象

  • メール送信
  • PDF/画像生成
  • 外部API同期
  • 大量集計
  • 監査ログや検索インデックス更新

9.2 UI側の見せ方(アクセシブル)

同期で待たせない代わりに、ユーザーには

  • 「開始しました」
  • 「進行状況」
  • 「完了しました(ダウンロードはこちら)」
    を分かりやすく提示します。
<div role="status" aria-live="polite" id="job-status">
  エクスポートを開始しました。完了すると通知します。
</div>

10. Redis:セッション/キャッシュ/キューの高速化

  • セッションを Redis にするとI/Oが減り、水平スケールしやすい
  • キャッシュも Redis で安定
  • Horizon と組み合わせるとキューの状況が可視化できる

注意

  • Redisは速いですが、キー設計が雑だとメモリを浪費します。TTLと上限を決めましょう。

11. フロント最適化:サーバだけ速くても体感は速くなりません

11.1 画像

  • まず最適化(WebP/AVIF、サイズ指定、遅延読み込み)
  • width/height を指定してレイアウトシフトを抑える

11.2 圧縮

  • gzip/brotli を有効化
  • 静的ファイルはCDNへ

11.3 JS/CSS

  • 使っていないJSを減らす
  • クリティカルCSSを優先
  • prefers-reduced-motion を尊重してアニメーションを控えめに

12. アクセシブルな“速い体験”:待ち時間の伝え方

速度改善と同じくらい大切なのが、「待っている間に不安にさせない」ことです。

12.1 ローディング中の明示

  • 更新領域に aria-busy="true"
  • 進捗や完了は role="status" で告知
  • スケルトンは装飾として使い、テキストの代替も用意
<section id="result" aria-busy="true" aria-live="polite">
  <p class="sr-only">読み込み中です。</p>
  {{-- スケルトン表示 --}}
</section>

12.2 再試行導線

読み込み失敗時は

  • 「もう一度試す」
  • 「時間をおいて試す」
  • 「軽量版を表示」
    のように選択肢を提示すると、行き止まりが減ります。

13. 最小の改善計画(段階導入のすすめ)

  1. 主要画面に計測(SQL数/時間、外部API時間)を追加
  2. N+1 を潰す(with/withCount
  3. 一覧を select+ページングに
  4. 高コスト集計を 5分キャッシュ
  5. 画像/PDF/メールをキュー化
  6. DBインデックスを当てる
  7. HTTPキャッシュ/ETag を導入
  8. 監視で性能劣化を検知できるようにする

14. よくある落とし穴と回避策

  • with() を入れ過ぎて逆に重い
    • 回避:重要画面の関連だけ、列も絞る
  • キャッシュが“古くて困る”
    • 回避:TTL短め+更新時の破棄、素材テーブル化
  • paginate() の COUNT が重い
    • 回避:必要なら simplePaginate()、または集計素材化
  • 外部APIを同期で呼び続ける
    • 回避:timeout/retry/fallback、可能なら非同期
  • スケルトンだけで状態が分からない
    • 回避:role="status" と短い文言を併置
  • 無限スクロールで迷子
    • 回避:「さらに読み込む」ボタン+読み上げ対応

15. チェックリスト(配布用)

計測

  • [ ] 主要画面のp95、SQL数、外部API時間、キュー遅延を可視化
  • [ ] 性能劣化のアラート(p95/5xx/遅延)を設定

DB/Eloquent

  • [ ] N+1 を with/withCount で解消
  • [ ] 列を select で最小化
  • [ ] 一覧はページング必須
  • [ ] インデックスがWHERE/ORDERに合っている

キャッシュ/非同期

  • [ ] 高コスト集計を Cache::remember
  • [ ] キャッシュキーにテナント/ロケールを含める
  • [ ] 重い処理をキュー化、Horizonで監視

HTTP/フロント

  • [ ] ETag/304 の条件付きリクエスト
  • [ ] gzip/brotli、CDN
  • [ ] 画像最適化(WebP/サイズ指定/遅延)

アクセシビリティ

  • [ ] aria-busyrole="status" で待ち状態を告知
  • [ ] 色に依存しない進捗表示
  • [ ] 失敗時の再試行導線
  • [ ] prefers-reduced-motion 尊重

16. まとめ

Laravelのパフォーマンスは、計測してボトルネックを型で潰せば、きれいに改善していきます。まずはN+1とページング、必要列の削減から。次にキャッシュとキュー化で“待たせない”設計へ進め、DBインデックスとHTTPキャッシュで土台を固めます。そして、速さはアクセシビリティと相性が良いです。待ち時間を短くするだけでなく、状況を短い言葉で知らせ、再試行や代替手段を用意することで、だれにとっても落ち着いて使える体験になります。


参考リンク

モバイルバージョンを終了