Laravelの応答が遅いとき、いきなりキャッシュやOctaneを導入しても、原因がデータベースなら十分な効果は得られません。
先に遅い処理を測り、不要な仕事を減らしてから、キャッシュ、非同期処理、実行基盤の順に検討すると、変更の効果と副作用を追いやすくなります。
この記事では、Laravelのパフォーマンス改善を、実装だけでなく障害検知と利用者への伝え方まで含めて整理します。
システム全体の設計や運用項目を先に整理したい場合は、PHPとLaravelで業務Webアプリを作る前に決めることも参照してください。
パフォーマンス改善の進め方
パフォーマンス最適化とは、応答時間、処理量、資源使用量などを測り、利用者と運用者に影響するボトルネックを減らす作業です。
可観測性とは、ログ、メトリクス、トレースなどを使い、システム内部で何が起きているかを外部から判断できる状態を指します。
両者は別々の作業ではありません。
改善前後を同じ指標で比較できなければ、速くなった理由も、次のリリースで遅くなった理由も説明できないからです。
| 段階 | 主な作業 | 確認すること |
|---|---|---|
| 1. 測る | 遅いリクエスト、クエリ、ジョブを特定する | 再現条件と基準値を残せるか |
| 2. 減らす | N+1、不要な列、重複計算を取り除く | 処理回数と取得量が減ったか |
| 3. 再利用する | アプリケーションキャッシュとHTTPキャッシュを設計する | 期限と無効化条件が明確か |
| 4. 分離する | 長い処理をキューへ移す | 再試行しても結果が壊れないか |
| 5. 基盤を整える | OPcache、Octane、CDNを検討する | 複雑さに見合う効果があるか |
| 6. 見守る | ダッシュボード、アラート、ロールバック手順を整える | 劣化を利用者より先に把握できるか |
最初に応答時間と失敗を測る
平均値だけでは、一部の利用者が経験する大きな遅延を見落とします。
中央値に当たるp50に加えて、遅い側の傾向を示すp95やp99、エラー率、単位時間当たりの処理量を同じ画面で追うと、変更の影響を判断しやすくなります。
SLO(サービスレベル目標)を置く場合は、事業上許容できる応答時間と成功率から決め、監視ツールの初期値をそのまま採用しないようにします。
ミドルウェアで処理時間を記録する例
class RequestTimer
{
public function handle($request, Closure $next)
{
$startedAt = microtime(true);
$response = $next($request);
$durationMs = (microtime(true) - $startedAt) * 1000;
Log::info('http.timing', [
'route' => optional($request->route())->getName(),
'status' => $response->getStatusCode(),
'duration_ms' => round($durationMs, 1),
'request_id' => $request->headers->get('X-Request-Id'),
]);
$response->headers->set('Server-Timing', "app;dur={$durationMs}");
return $response;
}
}
レスポンスヘッダーへ値を設定した後は、ヘッダー操作の戻り値ではなくレスポンス本体を返します。
Server-Timingはブラウザの開発者ツールで確認できますが、内部構成を推測できる指標を公開レスポンスへ出さないよう、公開範囲を設計してください。
ログには調査に必要な識別子を残し、アクセストークン、パスワード、セッション情報、不要な個人情報は記録しません。
Eloquentとデータベースの仕事を減らす
データベース起因の遅延は、キャッシュを追加する前にクエリの回数と取得量を減らすほうが原因を取り除きやすくなります。
N+1を一括取得へ置き換える
N+1問題とは、親データを一度取得した後、各行の関連データを個別に読み込み、行数に応じてクエリが増える状態です。
EloquentのEager Loadingを使うと、必要な関連を親データとまとめて取得できます。
$posts = Post::query()
->with([
'author:id,name',
'tags:id,name',
])
->latest()
->paginate(20);
関連先の列を絞るときは、リレーションの組み立てに必要な主キーと外部キーを落とさないようにします。
開発環境でLazy Loadingの違反を検出する設定も使えますが、本番の挙動を変える前に、既存画面への影響をテストしてください。
インデックスは実際のクエリから決める
WHERE、JOIN、ORDER BYに現れる列だからという理由だけで、各列へ個別にインデックスを増やすのは避けます。
絞り込み条件と並び順を含む実際のクエリを採取し、実行計画と件数の偏りを確認して、複合インデックスの列順を決めます。
並び順が同値になる可能性がある一覧では、created_atにidを加えるなど、ページをまたいでも順序が安定する条件を用意します。
ページネーション方式を画面要件に合わせる
総ページ数を表示しない画面なら、simplePaginate()を使うと総件数を求めるクエリを省けます。
$items = Item::query()
->orderByDesc('id')
->simplePaginate(50);
画面が総件数や最終ページへの移動を必要とするなら、件数取得を省く方式へ機械的に変えることはできません。
取得コストと必要な操作を並べて、画面ごとに選びます。
並び替えの入力を許可リストで制限する
リクエストで受け取った列名をそのままorderBy()へ渡すと、想定外の列や重い並び順が選ばれます。
$requested = (string) $request->input('sort', '-created_at');
$direction = str_starts_with($requested, '-') ? 'desc' : 'asc';
$column = ltrim($requested, '-');
$allowed = ['created_at', 'score'];
if (! in_array($column, $allowed, true)) {
$column = 'created_at';
$direction = 'desc';
}
$query->orderBy($column, $direction);
入力値の検証と、データベースが効率よく実行できる選択肢の制限を同じ場所で行う例です。
期限と無効化からキャッシュを設計する
キャッシュは「何を保存するか」より先に、「いつ古くなり、誰が消すか」を決めます。
商品の価格や権限のように誤った値を返せないデータと、多少古くても影響の小さい一覧では、許容できる保存時間が異なります。
デプロイ時のキャッシュ
Laravelは設定、イベント、ルート、Bladeビューをデプロイ時にキャッシュできます。
php artisan optimize
# 個別に実行する場合
php artisan config:cache
php artisan route:cache
php artisan view:cache
config:cacheを使う構成では、アプリケーションコードからenv()を直接読む箇所が残っていないかを確認します。
長時間動くキューワーカーやOctaneは、新しいコードを読み込めるよう、デプロイ手順に再読み込みまたは再起動を組み込みます。
アプリケーションデータのキャッシュ
final class CategoryRepository
{
public function all(): Collection
{
return Cache::remember(
'categories:all:v1',
now()->addHours(6),
fn () => Category::query()
->orderBy('rank')
->get(['id', 'name', 'slug'])
);
}
}
この例は、全利用者で共有でき、更新頻度が低い一覧を対象にしています。
作成、更新、削除の処理で同じキーを削除するか、キーのバージョンを切り替える手順まで実装して、古い値が残る時間を制御します。
検索条件、ページ番号、言語、権限で結果が変わる場合は、それらをキーへ含めるか、共有キャッシュの対象から外します。
HTTPキャッシュ
ETagはレスポンスの版を表す識別子で、ブラウザがIf-None-Matchで送り返した値と現在の版が一致すれば、サーバーは本文を含まない304 Not Modifiedを返せます。
CDNや共有キャッシュへ保存できる公開情報と、ログイン後の個別情報を分け、個別情報にはprivateなど適切なキャッシュ方針を使います。
ETagの引用符、弱い検証子、複数値の比較を独自コードで省略すると誤動作しやすいため、利用中のフレームワークやHTTPライブラリの条件付きリクエスト機能を使って検証します。
キューを再試行できる処理にする
帳票作成、メール送信、外部API連携などをキューへ移すと、Webリクエストは処理の完了を待たずに応答できます。
ただし、非同期化すると失敗が消えるのではなく、失敗する場所と時刻が変わります。
冪等性とは、同じ処理を再実行しても、二重請求や重複登録などの余分な結果を生まない性質です。
重複投入と同時実行を分ける
ShouldBeUniqueは、同じ識別子を持つジョブがすでにキューにあるとき、新しい投入を抑えるために使えます。
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
final class ExportOrders implements ShouldQueue, ShouldBeUnique
{
public function __construct(public int $userId) {}
public function uniqueId(): string
{
return (string) $this->userId;
}
public function handle(ExportService $service): void
{
$service->runFor($this->userId);
}
}
一方、すでに投入されたジョブ同士の実行時間が重ならないようにする目的には、WithoutOverlappingを検討します。
どちらもロックを支える共有キャッシュが前提になるため、複数サーバーでローカルキャッシュを使う構成では期待どおりに重複を防げません。
外部サービスの上限を守る
外部APIやメール配信には、ジョブミドルウェアでレート制限を適用し、上限を超えた処理を後へ回します。
短時間の呼び出し頻度と日次や月次の総量は別の制約なので、詳しい設計はLaravelで利用量制限を設計する方法で確認できます。
再試行回数、タイムアウト、待機時間は、処理の性質と接続先の制約から決めます。
OPcacheとOctaneの役割を分ける
OPcacheは、コンパイル済みのPHPバイトコードを共有メモリへ保存し、各リクエストで同じスクリプトを読み込み直して解析する仕事を減らします。
本番での有効化と更新方法は、PHPの設定、デプロイ方式、プロセス管理に合わせて決めます。
Laravel Octaneは、アプリケーションを一度起動してメモリに保持し、複数のリクエストを処理する実行方式です。
起動処理を繰り返さない利点がある一方、可変なグローバル状態、静的プロパティへの追加、リクエストを保持するシングルトンは、後続リクエストへの状態混入やメモリ増加を招きます。
Octane::tick()は一定間隔で処理を動かす機能であり、アプリケーションが作った状態を一般的に初期化する機能ではありません。
導入前に負荷試験を行い、メモリ使用量を監視し、ワーカーの最大リクエスト数と安全な再起動手順を決めます。
画像と静的アセット
- 画像には
widthとheightを指定し、読み込み前後のレイアウト移動を抑える。 srcsetとsizesで表示幅に合う画像を配信し、画面外の画像には用途を確認してloading="lazy"を使う。- CSSとJavaScriptはビルド時に不要コードを減らし、内容ハッシュ付きのファイル名で長期キャッシュしやすくする。
- CDNは利用地域、転送量、更新頻度を確認して導入し、キャッシュ無効化の手順も用意する。
HTTP/2を使っているという理由だけでファイルを一つにまとめるのではなく、初期表示に必要な量とキャッシュの再利用単位で分割します。
ログとメトリクスで原因を追えるようにする
ログは個別の出来事、メトリクスは時間に沿った数値の変化、トレースは一つの処理が複数のサービスを通る流れを追うために使います。
| 情報 | 確認できること | 例 |
|---|---|---|
| ログ | どの処理で何が起きたか | 例外、遅いクエリ、ジョブ失敗 |
| メトリクス | どの程度、いつから悪化したか | p95応答時間、エラー率、キュー待ち時間 |
| トレース | 時間を使った処理区間はどこか | Web、DB、外部API間の処理時間 |
Log::info('order.created', [
'order_id' => $order->id,
'request_id' => request()->header('X-Request-Id'),
]);
注文IDやリクエストIDのような相関キーがあれば、利用者からの問い合わせとサーバー側の記録を結び付けられます。
同じキーをすべてのログへ無条件に残すのではなく、目的、保存期間、閲覧権限を決めます。
HorizonとTelescopeの使い分け
- Horizon:キューの待ち時間、処理量、失敗したジョブを確認する。
- Telescope:リクエスト、クエリ、例外、ジョブなど、Laravelアプリケーション内の出来事を調査する。
Horizonのメトリクスを使う場合は、スナップショットの定期取得と保存数を設定します。
Telescopeを本番で使う場合は、環境設定だけに頼らず、ダッシュボードの認可、記録対象、データの保存期間を制限します。
読み込みと失敗をアクセシブルに伝える
画面が速くても、読み込み中なのか操作に失敗したのかが伝わらなければ、利用者は次の行動を選べません。
role="status"は、通常の操作位置からフォーカスを奪わず、状態の変化を支援技術へ伝えるために使います。
aria-busy="true"は、対象領域の更新が終わっていないことを示し、完了後にfalseへ戻します。
<section id="results" aria-busy="true" aria-describedby="load-status">
<p id="load-status" role="status">検索結果を読み込んでいます。</p>
<div class="skeleton" aria-hidden="true"></div>
</section>
<button type="button" aria-controls="results">
さらに読み込む
</button>
完了時はaria-busyをfalseへ変え、状態文を「10件追加しました」のように具体化します。
「さらに読み込む」操作で結果を末尾へ追加するだけなら、ボタンから自動的にフォーカスを移さず、利用者が続きの操作を選べる状態を保ちます。
画面遷移やダイアログ表示など、文脈そのものが変わる場合は、別途フォーカス管理を設計します。
読み込み表示を含む設計、実装、検証の進め方は、Webアクセシビリティを実務に組み込む方法も参考になります。
失敗時に示す内容
- 何が完了しなかったかを短い文で示す。
- 再試行、入力修正、簡易版への移動など、次に選べる操作を示す。
- 問い合わせに使えるリクエストIDがある場合は、機密情報を含まない形で表示する。
- 色だけに頼らず、見出し、文言、アイコンなどを組み合わせて状態を区別する。
外部障害の影響を限定する
外部APIやストレージへの接続では、接続までのタイムアウトと応答を待つタイムアウトを明示します。
一時的な失敗だけを指数バックオフで再試行し、入力不備や認可エラーのように再実行しても成功しない失敗は繰り返しません。
連続した失敗時に呼び出しを一時停止するサーキットブレーカーは、障害中の接続先へ処理を集中させないための仕組みです。
停止中に返す代替情報、復旧確認の条件、利用者への案内まで決めて初めて、影響範囲を狭められます。
データ更新では、トランザクションの範囲と外部通信の位置を確認し、途中失敗後にどこから再開するかを記録します。
小さく公開して再測定する
一度に複数の施策を入れると、改善または悪化の原因を切り分けにくくなります。
次の単位で変更し、各段階で同じ負荷条件と指標を使って比較します。
- 対象画面、再現手順、現在のp50とp95、エラー率を記録する。
- N+1、不要な列、重複処理を一つずつ減らす。
- 更新条件を説明できるデータだけをキャッシュする。
- 応答を待つ必要がない処理を、冪等性を確認してキューへ移す。
- 基盤変更は負荷試験と段階公開を行い、戻し方を用意する。
- 公開後の指標を基準値と比較し、次の改善対象を決める。
公開前のチェックリスト
- 遅い画面、クエリ、ジョブを再現できる。
- 変更前後を比較する指標と期間が決まっている。
- キャッシュの期限、キー、無効化条件、権限境界を説明できる。
- ジョブを再試行しても重複した結果が生じない。
- Octaneを使う場合、共有状態とメモリ使用量を検証した。
- 読み込み、完了、失敗が視覚と支援技術の両方へ伝わる。
- 監視画面の閲覧権限とデータ保存期間が決まっている。
- 問題が起きたときの停止条件とロールバック手順がある。
Laravel最適化で避けたい判断
| 避けたい判断 | 起きる問題 | 修正方針 |
|---|---|---|
| 計測前にキャッシュする | 本当のボトルネックが隠れる | クエリ回数と処理時間を先に測る |
| 長い期限だけを設定する | 更新後も古い値が残る | 無効化する処理と担当を決める |
| キューへ移せば完了とする | 再試行で重複結果が生じる | 冪等性、重複排除、タイムアウトを設計する |
| Octaneを先に導入する | 状態管理の複雑さが増える | 通常構成のボトルネックを減らしてから負荷試験する |
| 読み込み完了時に常にフォーカスを移す | 操作位置が予期せず変わる | 状態通知とフォーカス管理を分けて判断する |
結論
Laravelのパフォーマンスは、特定の機能を一つ導入すれば解決する問題ではありません。
遅延を測り、データベースと処理量を減らし、更新条件を説明できる範囲だけをキャッシュし、再試行できる処理をキューへ分けます。
その上でOPcacheやOctaneを検討し、ログとメトリクスで公開後の変化を追えば、速度と運用の安定性を同じ手順で改善できます。
読み込み中と失敗時の案内もこの手順に含めると、速さの数値だけでなく、利用者が状況を理解して操作を続けられる体験まで整えられます。
参考資料
- Laravel公式ドキュメント「Deployment」
- Laravel公式ドキュメント「Cache」
- Laravel公式ドキュメント「Eloquent Relationships」
- Laravel公式ドキュメント「Pagination」
- Laravel公式ドキュメント「Queues」
- Laravel公式ドキュメント「Octane」
- Laravel公式ドキュメント「Horizon」
- Laravel公式ドキュメント「Telescope」
- PHP公式マニュアル「OPcache」
- MDN「Cache-Control」
- MDN「ETag」
- MDN「Server-Timing」
- MySQL公式リファレンス「Optimization」
- W3C「Web Content Accessibility Guidelines 2.2」
- MDN「aria-busy」
この記事に関連する株式会社greedenの取り組み
性能改善は、原因の計測から安全な変更、公開後の監視までを一続きで考える必要があります。
株式会社greedenは、Laravelを含むWebシステムの新規開発、既存システムの改善、保守と監視を支援しています。
