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

[Field-Proven Complete Guide] Laravel Performance Optimization — Finding the Root Cause of Slowness, N+1 Fixes, Caching/HTTP Caching, Queueing, DB Indexes, Redis, Front-End Optimization, and an Accessible “Fast” Experience

What you’ll learn (key points)

  • A practical workflow to eliminate slowdowns via measure → hypothesize → improve → re-measure
  • How to identify common bottlenecks: N+1, unnecessary SELECTs, heavy aggregation, slow external APIs, slow I/O, etc.
  • Eloquent optimization (with/withCount/select/chunk/cursor) and DB index design
  • Caching (app/query/view/config), Redis, and HTTP caching (ETag/304)
  • Queueing and async processing (mail/images/PDFs/aggregations), Horizon monitoring
  • Front-end and delivery optimization (compression, HTTP/2, CDN, image optimization)
  • An accessible “fast experience”: screen-reader-friendly loading, skeleton pitfalls, prefers-reduced-motion, and how to communicate wait time

Target readers (who benefits?)

  • Laravel beginner–intermediate engineers: want to reproduce slowness causes and accumulate improvements
  • Tech leads / ops: want solid measurement and monitoring to stop regressions before deployment
  • Designers / QA / accessibility: want loading UI that “anyone can understand”

Accessibility level: ★★★★★

Includes screen-reader-friendly loading (aria-busy / role="status"), progress not relying on color alone, reduced motion, and keyboard-friendly flows that prevent getting lost during waits.


1. Introduction: Optimization Is Not “Gut Feeling,” It’s Measurement

Performance optimization isn’t about making things fast for its own sake—it’s a means to reduce user wait time, lower server costs, and prevent incidents. The quickest way to real results is to avoid guessing and repeatedly run measure → improve → re-measure.

Laravel is convenient, which also means it can become slow if you do nothing. But the upside is that the typical bottlenecks are well-known—if you eliminate them in order, you can make the app reliably faster.


2. Measure First: Identify What’s Slow

2.1 Define Your Goal (SLO) Up Front

Examples:

  • p95 of key pages under 300ms
  • p95 of APIs under 200ms
  • 5xx rate under 0.1%

If you don’t decide which screens matter, optimization never ends.

2.2 Core Measurement Points

  • App: request duration, number of SQL queries, SQL time, external HTTP time, queue delays
  • DB: slow queries, locks, index usage
  • Infra: CPU, memory, I/O, network, cache hit ratio
  • UX: TTFB, LCP, CLS, INP (Web Vitals)

2.3 Practical Tools on the Laravel Side

  • Telescope: visualize requests/SQL/exceptions in dev and validation
  • Logs: structured logging (trace_id + timings)
  • APM: Sentry / Datadog / New Relic (very strong if you can adopt one)

3. A Map of Typical Bottlenecks (The Order That Usually Pays Off First)

Listed in the order that tends to deliver results fastest in real projects:

  1. N+1 (too many SQL queries)
  2. Fetching unnecessary columns/rows (SELECT bloat, no paging)
  3. Heavy aggregations (COUNT / GROUP BY on every request)
  4. Slow/unstable external APIs
  5. Heavy work executed synchronously (image/PDF generation, etc.)
  6. No cache / ineffective cache
  7. Poor DB indexes
  8. Front-end delivery issues (no compression, heavy images, no CDN)

4. N+1 Fixes: The Absolute Basics of Eloquent

4.1 N+1 Example (Bad)

$posts = Post::latest()->take(20)->get();
foreach ($posts as $post) {
  echo $post->user->name; // likely triggers 20 extra queries
}

4.2 Eager Loading with with (Good)

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

4.3 Counts via withCount

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

4.4 Also Minimize Columns with select

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

Notes

  • Fewer columns = less memory + less transfer.
  • with is not magical. Overusing it can slow things down, so add it step-by-step starting from key screens.

5. DB Query Optimization: Paging, Indexes, and Slow Queries

5.1 Paging Is Mandatory

Returning “all rows” in a list is not only slow; it can also blow memory.
Use paginate() or simplePaginate() by default.

5.2 Index Design Rules of Thumb

  • Use composite indexes that match your WHERE/ORDER BY combinations
  • If you filter by tenant_id, include it first, e.g. (tenant_id, created_at)
  • Design indexes in the order of “most common conditions”

Example:

  • WHERE status = ? AND created_at >= ? ORDER BY created_at DESC
    → consider (status, created_at)

5.3 Reading EXPLAIN (Simplified)

  • If type=ALL (full table scan) appears, treat it as a red flag
  • Fix queries with extremely large rows estimates first
  • Using filesort isn’t always bad, but if it’s frequent, investigate

6. Large Data Processing: chunk / cursor / lazy

6.1 chunk

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

6.2 cursor (Memory-friendly)

foreach (User::where('active', true)->cursor() as $u) {
  // processes one by one, tends to use less memory
}

6.3 Caveats

  • cursor() doesn’t run one SQL per row; it streams via an iterator. But be careful with relationships (N+1 can reappear easily).
  • Bulk processing generally pairs well with queueing.

7. Caching: Biggest Impact for the Least Effort

7.1 Start with “High Cost, Low Change” Data

  • Top-page rankings
  • Navigation category lists
  • Dashboard aggregates
  • Configuration values (feature flags, plan limits)
$items = Cache::remember('top:popular', 300, function(){
  return Product::orderByDesc('popularity')->take(20)->get();
});

7.2 Cache Key Design

  • If data depends on locale/tenant/user, include them in the key
  • Example: t:{tenant_id}:home:popular:ja

7.3 Tagged Cache (Store-dependent)

It lets you invalidate related keys at once, which makes operations easier.


8. HTTP Caching: Save Bandwidth with ETag/304

For APIs and lists that rarely change, conditional requests are effective.

  • No change → 304
  • Changed → 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. Queueing: Don’t Make Users Wait for Heavy Work

9.1 Typical Async Candidates

  • Sending email
  • PDF/image generation
  • External API synchronization
  • Large aggregations
  • Audit logs and search index updates

9.2 UI Presentation (Accessible)

Instead of blocking synchronously, show users:

  • “Started”
  • “In progress”
  • “Completed (download here)”
<div role="status" aria-live="polite" id="job-status">
  Export started. You’ll be notified when it’s complete.
</div>

10. Redis: Faster Sessions / Cache / Queues

  • Redis sessions reduce I/O and make horizontal scaling easier
  • Cache via Redis is stable and fast
  • With Horizon, you can visualize queue health

Note

  • Redis is fast, but sloppy key design can waste memory. Define TTLs and caps.

11. Front-End Optimization: If Only the Server Is Fast, It Still Won’t Feel Fast

11.1 Images

  • Optimize first (WebP/AVIF, proper sizing, lazy loading)
  • Specify width/height to reduce layout shifts

11.2 Compression

  • Enable gzip/brotli
  • Put static assets on a CDN

11.3 JS/CSS

  • Reduce unused JS
  • Prioritize critical CSS
  • Respect prefers-reduced-motion and tone down animation

12. An Accessible “Fast Experience”: Communicating Wait Time

Just as important as making things faster is avoiding user anxiety while they wait.

12.1 Explicit Loading State

  • aria-busy="true" on the updating region
  • Announce progress/completion with role="status"
  • Use skeletons as decoration, and provide a text alternative too
<section id="result" aria-busy="true" aria-live="polite">
  <p class="sr-only">Loading.</p>
  {{-- skeleton UI --}}
</section>

12.2 Retry Paths

On load failure, offer choices like:

  • “Try again”
  • “Try later”
  • “Show a lightweight version”

This reduces dead ends.


13. Minimal Improvement Plan (Recommended Phased Adoption)

  1. Add measurement to key screens (SQL count/time, external API time)
  2. Fix N+1 (with / withCount)
  3. Convert lists to select + paging
  4. Cache high-cost aggregates for 5 minutes
  5. Queue image/PDF/mail work
  6. Add DB indexes
  7. Add HTTP caching / ETag
  8. Add monitoring to detect regressions

14. Common Pitfalls and How to Avoid Them

  • Overusing with() and making things heavier
    • Fix: eager load only critical relationships, and limit columns
  • Cache becomes “too stale” and hurts
    • Fix: shorter TTL + invalidate on updates, or create “materialized” summary tables
  • paginate() COUNT becomes expensive
    • Fix: use simplePaginate() where acceptable, or materialize aggregates
  • Calling external APIs synchronously forever
    • Fix: timeout/retry/fallback; async if possible
  • Skeleton UI alone makes the state unclear
    • Fix: pair with role="status" and a short message
  • Infinite scroll causes users to get lost
    • Fix: a “Load more” button + screen-reader announcements

15. Checklist (For Handout)

Measurement

  • [ ] Visualize p95, SQL count, external API time, and queue delay on key screens
  • [ ] Set alerts for regressions (p95 / 5xx / latency)

DB/Eloquent

  • [ ] Eliminate N+1 with with / withCount
  • [ ] Minimize columns with select
  • [ ] Paging for all lists
  • [ ] Indexes match WHERE/ORDER patterns

Caching/Async

  • [ ] Cache expensive aggregates with Cache::remember
  • [ ] Include tenant/locale in cache keys
  • [ ] Queue heavy work; monitor with Horizon

HTTP/Front-End

  • [ ] Conditional requests with ETag/304
  • [ ] gzip/brotli, CDN
  • [ ] Image optimization (WebP, correct sizes, lazy)

Accessibility

  • [ ] Announce loading with aria-busy and role="status"
  • [ ] Progress not dependent on color
  • [ ] Retry paths on failure
  • [ ] Respect prefers-reduced-motion

16. Summary

Laravel performance improves cleanly when you measure and eliminate bottlenecks by known patterns. Start with N+1, paging, and column minimization. Then move to caching and queueing for a “don’t make users wait” design, and solidify the foundation with DB indexes and HTTP caching. And speed pairs well with accessibility: don’t just shorten waits—announce status in short text, provide retries and alternatives, and make the experience calm and usable for everyone.


Reference Links

By greeden

Leave a Reply

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

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