A slow Laravel application frustrates users and costs money. But with proper optimization, you can handle 10x more traffic without adding servers. At ZIRA Software, we've optimized Laravel apps from 2-second page loads to sub-100ms responses. This guide shares our battle-tested techniques.
Measure First, Optimize Second
Never optimize blind. Measure everything:
Install Laravel Debugbar
composer require barryvdh/laravel-debugbar --dev
Shows queries, memory usage, view rendering time per request.
Install Clockwork
composer require itsgoingd/clockwork
Browser extension for profiling Laravel apps.
Enable Query Logging
AppServiceProvider.php:
<?php namespace App\Providers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
if (config('app.debug')) {
DB::listen(function ($query) {
\Log::info(
$query->sql,
[
'bindings' => $query->bindings,
'time' => $query->time
]
);
});
}
}
}
Database Optimization
1. Fix N+1 Queries
Problem:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // N+1 query!
}
This executes 1 query for posts + N queries for authors.
Solution - Eager Loading:
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name; // No additional queries!
}
Multiple relationships:
$posts = Post::with('author', 'comments', 'tags')->get();
Nested relationships:
$posts = Post::with('comments.author')->get();
Conditional eager loading:
$posts = Post::with(['comments' => function ($query) {
$query->where('approved', true)
->orderBy('created_at', 'desc')
->limit(5);
}])->get();
2. Select Only Needed Columns
Bad:
$users = User::all(); // Selects all columns
Good:
$users = User::select('id', 'name', 'email')->get();
For relationships:
$posts = Post::with('author:id,name')->get();
3. Use Indexes
Migration:
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->integer('user_id')->unsigned();
$table->timestamp('published_at')->nullable();
// Add indexes
$table->index('user_id');
$table->index('published_at');
$table->index(['user_id', 'published_at']); // Composite index
});
Check query execution:
EXPLAIN SELECT * FROM posts WHERE user_id = 1 AND published_at IS NOT NULL;
4. Chunk Large Datasets
Bad - loads all into memory:
$users = User::all(); // Out of memory with 100k users!
foreach ($users as $user) {
// Process
}
Good - processes in batches:
User::chunk(100, function ($users) {
foreach ($users as $user) {
// Process
}
});
Or use cursor (Laravel 5.2+):
foreach (User::cursor() as $user) {
// Process one at a time
}
5. Cache Query Results
<?php namespace App\Repositories;
use App\Post;
use Illuminate\Support\Facades\Cache;
class PostRepository
{
public function getAllPublished()
{
return Cache::remember('posts.published', 60, function () {
return Post::with('author')
->whereNotNull('published_at')
->orderBy('published_at', 'desc')
->get();
});
}
public function find($id)
{
return Cache::remember("posts.{$id}", 60, function () use ($id) {
return Post::with('author', 'tags')
->findOrFail($id);
});
}
public function create(array $data)
{
$post = Post::create($data);
// Clear cache
Cache::forget('posts.published');
return $post;
}
}
6. Database Connection Pooling
Use persistent connections:
DB_PERSISTENT=true
Use pgbouncer for PostgreSQL:
sudo apt-get install pgbouncer
Caching Strategies
1. Route Caching
php artisan route:cache
Never use closures in routes.php when caching:
Bad:
Route::get('/', function () {
return view('welcome');
});
Good:
Route::get('/', 'HomeController@index');
2. Config Caching
php artisan config:cache
Don't use env() in code after caching - it returns null!
Bad:
$apiKey = env('API_KEY'); // null after config:cache!
Good:
$apiKey = config('services.api.key');
3. View Caching
php artisan view:cache
4. Redis for Cache
Install predis:
composer require predis/predis
.env:
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_DRIVER=redis
config/database.php:
'redis' => [
'client' => 'predis',
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 1,
],
],
5. Full Page Caching
Middleware:
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Cache;
class CacheResponse
{
public function handle($request, Closure $next, $minutes = 10)
{
if ($request->method() !== 'GET') {
return $next($request);
}
$key = 'route-' . md5($request->url());
if (Cache::has($key)) {
return response(Cache::get($key));
}
$response = $next($request);
if ($response->status() === 200) {
Cache::put($key, $response->getContent(), $minutes);
}
return $response;
}
}
Use in routes:
Route::get('/', 'HomeController@index')->middleware('cache.response:60');
6. CDN for Static Assets
Use Laravel Mix versioning:
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.version();
In views:
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<script src="{{ mix('js/app.js') }}"></script>
Configure CDN:
CDN_URL=https://cdn.example.com
Asset helper:
function cdn_asset($path)
{
return env('CDN_URL', '') . '/' . ltrim($path, '/');
}
Code Optimization
1. Autoloader Optimization
composer dump-autoload --optimize
2. Use Eager Loading Counts
Bad:
$users = User::all();
foreach ($users as $user) {
echo $user->posts->count(); // N+1 queries!
}
Good:
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo $user->posts_count; // No queries!
}
3. Avoid Accessors in Lists
Model with accessor:
class User extends Model
{
public function getFullNameAttribute()
{
return $this->first_name . ' ' . $this->last_name;
}
}
Bad - runs accessor for each record:
$users = User::all();
foreach ($users as $user) {
echo $user->full_name; // Computed 1000 times!
}
Good - use raw SQL:
$users = User::selectRaw('*, CONCAT(first_name, " ", last_name) as full_name')->get();
foreach ($users as $user) {
echo $user->full_name; // Already computed!
}
4. Optimize Collection Operations
Bad - multiple iterations:
$users = User::all();
$active = $users->filter(function ($user) {
return $user->active;
});
$names = $active->map(function ($user) {
return $user->name;
});
Good - single iteration:
$names = User::where('active', true)
->pluck('name');
5. Queue Long-Running Tasks
Bad:
public function store(Request $request)
{
$user = User::create($request->all());
// Blocks response for 5 seconds!
Mail::to($user)->send(new WelcomeEmail($user));
return redirect('/dashboard');
}
Good:
public function store(Request $request)
{
$user = User::create($request->all());
// Queued - instant response!
dispatch(new SendWelcomeEmail($user));
return redirect('/dashboard');
}
Session Optimization
Use Database or Redis Sessions
.env:
SESSION_DRIVER=redis
Don't store large objects in session:
Bad:
session(['products' => Product::all()]);
Good:
session(['product_ids' => Product::pluck('id')]);
Asset Optimization
1. Minify Assets
webpack.mix.js:
const mix = require('laravel-mix');
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.minify('public/js/app.js')
.minify('public/css/app.css')
.version();
if (mix.inProduction()) {
mix.version();
}
2. Image Optimization
Install intervention/image:
composer require intervention/image
Resize on upload:
use Intervention\Image\Facades\Image;
$image = $request->file('photo');
Image::make($image)
->resize(800, null, function ($constraint) {
$constraint->aspectRatio();
})
->save(public_path('uploads/' . $filename));
3. Lazy Load Images
<img src="placeholder.jpg" data-src="actual-image.jpg" class="lazy">
<script>
document.addEventListener("DOMContentLoaded", function() {
const lazyImages = document.querySelectorAll('.lazy');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
lazyImages.forEach(img => imageObserver.observe(img));
});
</script>
Server Optimization
1. Use OPcache
php.ini:
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=0
opcache.validate_timestamps=0 # Production only!
Clear OPcache after deployment:
php artisan optimize:clear
2. PHP-FPM Tuning
/etc/php/7.0/fpm/pool.d/www.conf:
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500
3. Nginx Optimization
/etc/nginx/nginx.conf:
worker_processes auto;
worker_connections 1024;
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss;
# Browser caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# FastCGI cache
fastcgi_cache_path /var/cache/nginx levels=1:2
keys_zone=microcache:10m max_size=1g inactive=1h;
location ~ \.php$ {
fastcgi_cache microcache;
fastcgi_cache_valid 200 1h;
fastcgi_cache_bypass $http_pragma $http_authorization;
fastcgi_no_cache $http_pragma $http_authorization;
}
}
Monitoring and Profiling
Install Laravel Telescope
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
Access at /telescope to see:
- Slow queries
- Failed jobs
- Exceptions
- Request duration
New Relic APM
Install PHP agent:
wget -O - https://download.newrelic.com/548C16BF.gpg | apt-key add -
echo "deb http://apt.newrelic.com/debian/ newrelic non-free" > /etc/apt/sources.list.d/newrelic.list
apt-get update
apt-get install newrelic-php5
newrelic-install install
.env:
NEW_RELIC_APP_NAME=MyLaravelApp
NEW_RELIC_LICENSE_KEY=your-key
Custom Timers
use Illuminate\Support\Facades\Log;
$start = microtime(true);
// Your code here
$duration = (microtime(true) - $start) * 1000;
Log::info('Operation completed', ['duration' => $duration . 'ms']);
Load Testing
Install Apache Bench:
apt-get install apache2-utils
Test:
ab -n 1000 -c 10 http://yoursite.com/
Install Locust:
pip install locust
locustfile.py:
from locust import HttpLocust, TaskSet, task
class UserBehavior(TaskSet):
@task(1)
def index(self):
self.client.get("/")
@task(2)
def posts(self):
self.client.get("/posts")
class WebsiteUser(HttpLocust):
task_set = UserBehavior
min_wait = 1000
max_wait = 5000
Run:
locust -f locustfile.py --host=http://yoursite.com
Performance Checklist
- [ ] Fix N+1 queries (use Debugbar)
- [ ] Add database indexes
- [ ] Cache expensive queries
- [ ] Use route/config/view caching
- [ ] Move to Redis for cache/sessions
- [ ] Queue long-running tasks
- [ ] Optimize images
- [ ] Enable OPcache
- [ ] Use CDN for assets
- [ ] Monitor with Telescope/NewRelic
- [ ] Load test before launch
Conclusion
Performance optimization is continuous, not one-time. Measure, optimize hotspots, measure again. At ZIRA Software, these techniques have helped clients handle 10x traffic spikes without infrastructure changes.
Start with the database - fix N+1 queries and add indexes. Then add caching. Then optimize code. Measure every step.
Need help optimizing your Laravel application? Contact ZIRA Software to discuss performance audits and optimization strategies that deliver measurable results.