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:
- Laravel Telescope: For a deep dive into queries, requests, and slow jobs.
- Laravel Debugbar: For real-time profiling during development.
- Blackfire.io: For a granular, function-level performance profile.
- 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
- 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(); - Image Optimization: I enforced image compression and used lazy loading.
<img src="{{ $image->url }}" alt="{{ $image->alt }}" loading="lazy"> - Leveraging Browser Caching: I configured the server to serve static assets with far-future expiration headers.
The Final Result & Key Takeaways
| Metric | Before | After | Improvement |
|---|---|---|---|
| Page Load Time | 4200 ms | 1250 ms | ~70% Faster |
| Database Queries | 150+ | 8 | ~95% Reduction |
| Largest Contentful Paint | 3.8s | 1.1s | ~71% Faster |
| Google PageSpeed Score | 42 | 88 | Mobile-Friendly |
My Performance Audit Checklist:
- 🔍 Profile First, Assume Later: Always use a tool like Telescope or Blackfire. Don’t guess where the bottleneck is.
- 🗃️ Eager Load Aggressively: N+1 queries are the #1 performance killer. Use
with()andload()everywhere. - 📚 Index Your Database: Queries are only as fast as your indexes. Use
EXPLAINon slow queries. - 💾 Cache Strategically: If data doesn’t change often, cache it. Use Redis for in-memory speed.
- ⚡ 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.