Laravel Performance Audit: How I Cut Load Time by 70% on a Live App

Real case: Cut Laravel load time by 70% using Telescope, eager loading, and caching. Full audit process.

Laravel Performance Audit: How I Cut Load Time by 70% on a Live App

Laravel Performance Audit: How I Cut Load Time by 70% on a Live App

A few months ago, a client came to me with a common problem: their Laravel SaaS application was getting slower as they grew. User complaints were rising, and Google PageSpeed Insights scores were… depressing. The initial page load was hovering around 4.2 seconds. After a thorough performance audit, we got it down to a snappy 1.3 seconds.

This wasn’t magic; it was a systematic process. Here’s the exact blueprint I followed, so you can do the same for your applications.

The “Before” Picture: What We Were Working With

  • Application: A B2B SaaS platform built on Laravel 9.
  • Problem: Dashboard page was painfully slow (~4200ms load time).
  • Symptoms: High server CPU during peak hours, slow Time to First Byte (TTFB).
  • Tech Stack: Laravel, MySQL, Redis (configured but underutilized), Blade templates.

The Performance Audit Toolkit

Before you can fix problems, you need to see them. My go-to tools for a Laravel audit are:

  1. Laravel Telescope: For a deep dive into queries, requests, and slow jobs.
  2. Laravel Debugbar: For real-time profiling during development.
  3. Blackfire.io: For a granular, function-level performance profile.
  4. Chrome DevTools: For network and front-end analysis.

The 4 Biggest Problems & How I Fixed Them

Problem #1: The N+1 Query Avalanche

The Finding: Using Telescope, I immediately spotted the culprit. The dashboard was making over 150 database queries to load a simple list of user projects and their tasks.

The Code Smell:

// In the Controller (The Problem)
$projects = Project::where('user_id', auth()->id())->get();

// In the Blade Template (The Disaster)
@foreach($projects as $project)
    <h2>{{ $project->name }}</h2>
    @foreach($project->tasks as $task) <!-- N+1 Query Here! -->
        <p>{{ $task->name }}</p>
    @endforeach
@endforeach

Every loop in the blade was making a new query: SELECT * FROM tasks WHERE project_id = ?.

The Fix: Eager Loading

// In the Controller (The Fix)
$projects = Project::with('tasks') // Eager load the relationship!
            ->where('user_id', auth()->id())
            ->get();

This single change reduced the queries from 150+ to just 2. The result was an immediate ~1.5 second drop in load time.


Problem #2: The Unoptimized “Chonky” Query

The Finding: One of the remaining queries was still taking 800ms. It was a complex report query with multiple JOINs and GROUP BY clauses, calculating aggregates on a massive table.

The Fix: Database Indexing & Query Refactoring

First, I used EXPLAIN to analyze the query and found a full table scan on the project_id column in the tasks table.

-- Adding the missing index
ALTER TABLE `tasks` ADD INDEX `tasks_project_id_index` (`project_id`);

Then, I refactored the Eloquent query to be more selective and avoid unnecessary SELECT *:

// Before: Heavy and slow
$reportData = Task::join(...)// Complex joins
            ->groupBy(...)
            ->get();

// After: Lean and targeted
$reportData = Task::selectRaw('COUNT(*) as task_count, DATE(created_at) as date')
            ->where('project_id', $projectId)
            ->with('project') // Eager load instead of join if possible
            ->groupBy('date')
            ->get();

Result: The query time dropped from 800ms to under 90ms.


Problem #3: Cache? What Cache?

The Finding: The dashboard’s “recent activity” feed and static configuration data were being re-queried on every single page load, even though this data only changes a few times a day.

The Fix: Strategic Caching with Redis

I implemented Laravel’s cache facade to store expensive data.

// In the Controller
use Illuminate\Support\Facades\Cache;

$recentActivity = Cache::remember('user:' . auth()->id() . ':recent_activity', 3600, function () {
    return Activity::where('user_id', auth()->id())
                ->with('subject')
                ->latest()
                ->limit(50)
                ->get();
});

// Even simple configuration data can be cached
$categories = Cache::rememberForever('categories', function () {
    return Category::all();
});

Result: For returning users, the page now loaded almost instantly from cache, saving another ~500ms.


Problem #4: The Front-End Bottleneck

The Finding: Chrome DevTools showed a massive render-blocking CSS file and unoptimized images. The TTFB was now good, but the page still felt slow.

The Fix: Asset Optimization & Laravel Mix

  1. Code Splitting: I used Laravel Mix to split my CSS and JS. // webpack.mix.js mix.js('resources/js/app.js', 'public/js') .postCss('resources/css/app.css', 'public/css', []) .version();
  2. Image Optimization: I enforced image compression and used lazy loading. <img src="{{ $image->url }}" alt="{{ $image->alt }}" loading="lazy">
  3. Leveraging Browser Caching: I configured the server to serve static assets with far-future expiration headers.

The Final Result & Key Takeaways

MetricBeforeAfterImprovement
Page Load Time4200 ms1250 ms~70% Faster
Database Queries150+8~95% Reduction
Largest Contentful Paint3.8s1.1s~71% Faster
Google PageSpeed Score4288Mobile-Friendly

My Performance Audit Checklist:

  1. 🔍 Profile First, Assume Later: Always use a tool like Telescope or Blackfire. Don’t guess where the bottleneck is.
  2. 🗃️ Eager Load Aggressively: N+1 queries are the #1 performance killer. Use with() and load() everywhere.
  3. 📚 Index Your Database: Queries are only as fast as your indexes. Use EXPLAIN on slow queries.
  4. 💾 Cache Strategically: If data doesn’t change often, cache it. Use Redis for in-memory speed.
  5. ⚡ Optimize Assets: A fast back-end is let down by bloated front-end assets.

Performance work is never truly “done,” but by applying these systematic steps, we transformed a sluggish application into a fast, scalable product that keeps users happy.