php elephant sticker
Photo by RealToughCandy.com on Pexels.com
Table of Contents

【Practical Complete Guide】Laravel Performance Optimization & Observability — Caching, DB Design, Job Splitting, Octane, Metrics, and Accessible Loading States

What you’ll learn (key points)

  • Designing caches (Config/Route/View/Query) and HTTP caching
  • Eliminating Eloquent N+1s, index design, optimizing aggregation & pagination
  • Job splitting, throttling, de-duplication, idempotent handling
  • Octane (Swoole/RoadRunner), OPcache, and optimal static-asset delivery
  • Monitoring with Horizon, Telescope, structured logs, and metrics (latency/error rate)
  • Accessible loading UI, progressive enhancement, progress indications not reliant on color

Intended readers (who benefits?)

  • Laravel beginner–intermediate engineers: raise speed and stability step by step
  • Tech leads/architects: standardize operations-ready design with observability
  • SRE/QA: run metric-driven performance testing and regression detection
  • Designers/writers: craft accessible loading displays and skeleton UIs

1. Guiding principles: “Reduce waste first,” then “get faster by design”

If you optimize in the wrong order, only complexity grows.

  1. Measure: identify slow requests/queries/jobs.
  2. Reduce: N+1s, unnecessary queries, wasted rendering.
  3. Cache: data, templates, HTTP.
  4. Asynchronize: push heavy work to jobs.
  5. Harden the platform: OPcache, Octane, CDN.
  6. Observe: detect regressions and enable rollbacks.

Below are concrete steps for safely stacking improvements.


2. Caching strategy: maximize hit surface, control invalidation

2.1 Start with built-in caches

php artisan config:cache
php artisan route:cache
php artisan view:cache
  • Cut startup overhead via compiled config/routes/Blade caches.
  • In production, OPcache is a given. Have deploy/restart refresh caches automatically.

2.2 Data cache (example in Repository layer)

class CategoryRepo {
  public function all(): Collection {
    return Cache::remember('categories:all', now()->addHours(6), function () {
      return Category::query()->orderBy('rank')->get(['id','name','slug']);
    });
  }
}
  • Key naming: think resource:conditions:version.
  • Locality: target lists/dictionaries. For user-specific data, keep TTL short or use another layer.

2.3 Short-lived query cache

$top = Cache::remember("posts:top:{$page}", 60, fn() =>
  Post::with('author')
      ->published()
      ->orderByDesc('score')
      ->paginate(20)
);
  • Short-lived cache for heavy lists with pagination + with().
  • Invalidate via events (on create/update/delete call Cache::forget).

2.4 HTTP cache (ETag/Last-Modified)

$etag = sha1($post->updated_at.$post->id);
if (request()->header('If-None-Match') === $etag) {
  return response()->noContent(304);
}
return response()->view('post.show', compact('post'))
  ->header('ETag', $etag)
  ->header('Cache-Control', 'public, max-age=120');
  • Combine with a CDN to reduce bandwidth.
  • For authenticated pages, prefer private and short lifetimes.

3. Eloquent optimization: N+1, indexes, aggregation

3.1 Detect and eliminate N+1s

// Example: fetch author and tags together
$posts = Post::with(['author:id,name','tags:id,name'])
  ->latest()->paginate(20);
  • Use with() to eager load relations; select only needed columns.
  • For aggregate displays, consider counter caches or pre-aggregated tables.

3.2 Index design

  • Add composite indexes to columns in WHERE/ORDER BY/JOIN.
  • For created_at DESC ordering, stabilize with (created_at, id), etc.
  • For LIKE searches, prefer prefix matches (column LIKE 'abc%'); consider a separate store for full-text needs.

3.3 Whitelist search/sort options

$sort = $req->enum('sort', ['-created_at','created_at','-score','score']) ?? '-created_at';
$dir  = str_starts_with($sort,'-') ? 'desc' : 'asc';
$col  = ltrim($sort,'-');
$query->orderBy($col, $dir);
  • Ban arbitrary orderBy; whitelist to keep to optimized paths.

3.4 Separate aggregation

  • For heavy dashboard aggregates, materialize periodically and keep the view read-only/lightweight.
  • If real-time is unnecessary, update asynchronously.

4. View optimization: Blade and front-end slimming

4.1 Conditionals & components

  • Reduce @include within large loops; render in batches of collections.
  • Preformat heavy helpers via ViewModels or Accessors.

4.2 Images & static assets

  • Set width/height on <img> to suppress CLS.
  • Use <picture>, srcset, and loading="lazy" to cut bytes.
  • With HTTP/2, bundle smartly; tree-shake unused CSS/JS at build time.

4.3 Accessible loading states

<div role="status" aria-live="polite" class="mb-2">
  Loading data…
</div>
<ul aria-busy="true" aria-describedby="loading-desc">
  <li class="skeleton h-6 w-full"></li>
  <li class="skeleton h-6 w-5/6 mt-2"></li>
</ul>
<p id="loading-desc" class="sr-only">The list will appear once loading completes.</p>
  • Skeleton UIs must include text descriptions.
  • After completion, set aria-busy="false" and move focus to the list head to restore context.

5. Asynchrony: job splitting, de-dup, throttling

5.1 Offload heavy work to jobs

class ExportOrders implements ShouldQueue {
  public $tries = 3;
  public $timeout = 1800; // 30 min
  public function handle(ExportService $svc) { $svc->run(); }
}
  • Ensure idempotency (same input, same output) so retries don’t break state.

5.2 De-duplication (unique jobs)

$lock = Cache::lock("export:{$userId}", 600);
if ($lock->get()) {
  ExportOrders::dispatch($userId)->onQueue('exports');
  // Release $lock->release() when the job finishes
}
  • Serialize same-type work per user to protect resources.

5.3 Rate-limit middleware

public function middleware(): array {
  return [ new \Illuminate\Queue\Middleware\RateLimited('exports') ];
}
  • Apply throttling for external APIs and email sends.

5.4 Front-end fallback

  • If real-time push is unavailable, use polling.
  • While loading use role="status", and on failure provide brief cause + next steps.

6. Octane/OPcache/process tuning

6.1 OPcache basics

  • In production: opcache.enable=1, opcache.validate_timestamps=0 (for immutable builds).
  • Restart processes on deploy to refresh caches.

6.2 Where Laravel Octane helps

  • Cuts synchronous I/O overhead.
  • Share session/cache/queues via external stores.
  • Avoid stateful singletons and cross-request pollution; periodically re-init (e.g., Octane::tick()).

6.3 Serve images/statics via CDN

  • If the app doesn’t have to serve it, serve via CDN.
  • Use signed URLs for time-limited access.

7. Observability: logs, traces, metrics

7.1 Structured logs

Log::info('order.created', [
  'order_id' => $order->id,
  'user_id' => $order->user_id,
  'amount' => $order->amount,
  'request_id' => request()->header('X-Request-Id')
]);
  • Always log searchable keys (user ID, order ID, request ID).
  • Mask PII, never log tokens/secrets.

7.2 Latency, error rate, throughput

  • Track percentiles (p50/p95/p99) to reflect user experience.
  • “Slow ≠ bad” — define SLOs and detect deviations.
  • Compare metrics per deploy; roll back on regressions.

7.3 Telescope/Horizon

  • Telescope: visibility into requests/queries/exceptions/jobs.
  • Horizon: queue backlogs, failed jobs, processing time dashboards.
  • Lock down in production; enable briefly for forensic purposes.

8. Pagination & incremental loading

8.1 simplePaginate and infinite scroll

$items = Item::orderByDesc('id')->simplePaginate(50);
  • Avoid the cost of total-count queries.
  • For infinite scroll, also provide a manual control (“Load more” button).

8.2 Accessible loading

<button id="more" class="btn" aria-controls="list" aria-describedby="load-hint">
  Load more
</button>
<p id="load-hint" class="sr-only">Additional results will be appended at the bottom.</p>
<div id="alist" role="feed" aria-busy="false"></div>
  • Ensure it’s operable even without auto-loading.

9. Make failures harmless: timeouts, retries, circuit breakers

  • For HTTP clients, set explicit connect/response timeouts.
  • Retry transient errors with exponential backoff.
  • For persistent failures, open a circuit temporarily and show fallback copy in the UI.
  • Limit blast radius with scopes (transactions/partial updates).

10. i18n & time-zone performance

  • Set Carbon locale once at boot.
  • Provide translations via JSON or named keys to improve cache hit rates.
  • Consolidate currency/date formatting on the server; minimize client-side computation.

11. Security vs. speed balance

  • Apply stricter rate limits to more sensitive operations.
  • Enforce CSP/HSTS/Referrer-Policy via headers, and if using dynamic nonces, minimize template overhead.
  • For signed URLs and auth checks, be mindful of routes that must bypass caches.

12. UX: guidance for slow, failed, and offline states

12.1 Slow-response guidance

<div id="status" role="status" aria-live="polite">
  No response for over 3 seconds. Please check your connection.
</div>
  • As time elapses, show short guidance and next steps (reload, lite mode, etc.).

12.2 Failure guidance

  • Put a “Try again” button front and center.
  • Display a request ID usable for support.
  • Don’t rely on color alone—use icon + text for states.

13. Implementation samples: from measuring bottlenecks to fixes

13.1 Measure response time via middleware

class RequestTimer {
  public function handle($req, Closure $next) {
    $start = microtime(true);
    $res = $next($req);
    $ms = (int)((microtime(true) - $start) * 1000);
    Log::info('http.timing', [
      'path' => $req->path(),
      'status' => $res->getStatusCode(),
      'ms' => $ms,
      'request_id' => $req->headers->get('X-Request-Id'),
    ]);
    return $res->headers->set('Server-Timing', "app;dur={$ms}");
  }
}
  • In the browser Network panel, inspect Server-Timing to spot slow views.

13.2 Visualize N+1s (dev only)

DB::listen(function ($query) {
  if (str_contains($query->sql, 'select') && $query->time > 30) {
    logger()->debug('slow.query', ['sql' => $query->sql, 'ms' => $query->time]);
  }
});
  • Log queries above a threshold.

13.3 Fundamental query improvements

  • Use withCount and selectRaw to push aggregation onto the DB.
  • Accumulate history/stats in separate tables to avoid write-heavy hot paths.

14. Checklist (for distribution)

Measurement

  • [ ] p50/p95/p99, error rate, throughput
  • [ ] slow query / slow request logs
  • [ ] Dashboard comparing with the latest deploy

Reduction

  • [ ] N+1 eliminated (with() / column minimization)
  • [ ] Appropriate indexes
  • [ ] Pruned recalculation / template branching

Caching

  • [ ] Config/Route/View/OPcache
  • [ ] Short-lived data cache + invalidation strategy
  • [ ] HTTP ETag/Last-Modified/CDN

Asynchrony

  • [ ] Jobify + retries + timeouts
  • [ ] Unique jobs / rate limiting
  • [ ] Fallbacks (polling / lite mode)

Platform

  • [ ] Consider Octane coverage
  • [ ] CDN for static assets
  • [ ] Image srcset / lazy-load / width & height

Observability

  • [ ] Structured logs & request IDs
  • [ ] Safe operation of Horizon/Telescope
  • [ ] Alert thresholds & rollback procedures

Accessibility

  • [ ] Loading copy + role="status"
  • [ ] State indications beyond color; focus restoration
  • [ ] Next actions on failure

15. Common pitfalls and how to avoid them

  • Caching while N+1s remain → Eliminate first, cache second.
  • Long-lived caches without invalidation → Event-driven invalidation.
  • Passing arbitrary sort/search straight through → Whitelist for optimized paths.
  • Feeling safe after “just async” → Without retries/idempotency, failures pile up.
  • Silent loading states → Provide brief guidance and ways to act.
  • Optimizing without measurement → You won’t know impact; run measure → improve → re-measure.

16. Summary

  • Advance in order: measure → reduce → cache → async → platform hardening → observe.
  • For Eloquent, use with() + indexes and externalize aggregates to lighten fundamentals.
  • Make jobs resilient with de-dup, rate limits, idempotency.
  • Boost baseline speed with OPcache/Octane/CDN.
  • Keep loading/failure/retry flows accessible.
  • Put metrics on dashboards to spot and roll back regressions early.

Performance isn’t a one-off fix—it’s a habit of measuring and improving. Use this template to grow a team-wide Laravel that’s fast and understandable.


References

By greeden

Leave a Reply

Your email address will not be published. Required fields are marked *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)