【現場完全ガイド】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. 典型ボトルネックの地図(最初に潰す順番)
現場で成果が出やすい順に並べます。
- N+1(SQLが増えすぎ)
- 必要のないカラム/行を取る(SELECT過多、ページングなし)
- 重い集計(COUNT/GroupByが毎回走る)
- 外部APIが遅い/不安定
- 画像/PDF生成など重い処理を同期実行
- キャッシュが無い/効いていない
- DBインデックスが不適切
- フロント配信(圧縮なし/画像が重い/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. 最小の改善計画(段階導入のすすめ)
- 主要画面に計測(SQL数/時間、外部API時間)を追加
- N+1 を潰す(
with/withCount) - 一覧を
select+ページングに - 高コスト集計を 5分キャッシュ
- 画像/PDF/メールをキュー化
- DBインデックスを当てる
- HTTPキャッシュ/ETag を導入
- 監視で性能劣化を検知できるようにする
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-busyとrole="status"で待ち状態を告知 - [ ] 色に依存しない進捗表示
- [ ] 失敗時の再試行導線
- [ ]
prefers-reduced-motion尊重
16. まとめ
Laravelのパフォーマンスは、計測してボトルネックを型で潰せば、きれいに改善していきます。まずはN+1とページング、必要列の削減から。次にキャッシュとキュー化で“待たせない”設計へ進め、DBインデックスとHTTPキャッシュで土台を固めます。そして、速さはアクセシビリティと相性が良いです。待ち時間を短くするだけでなく、状況を短い言葉で知らせ、再試行や代替手段を用意することで、だれにとっても落ち着いて使える体験になります。
