Reference
Comprehensive guide to optimizing Laravolt v7 applications for production performance.
Overview
Performance optimization areas:
- Database: Query optimization, indexing, eager loading
- Caching: Application cache, query cache, view cache
- Assets: Minification, compression, CDN
- Server: Octane, FrankenPHP, opcache
- Monitoring: Profiling, APM, logging
Database Optimization
Query Optimization
Problem: N+1 Query Problem
// ❌ Bad: N+1 queries (1 + N for each product's category)
$products = Product::all();
foreach ($products as $product) {
echo $product->category->name; // Triggers query per product
}Solution: Eager Loading
// ✅ Good: 2 queries total (1 for products, 1 for categories)
$products = Product::with('category')->get();
foreach ($products as $product) {
echo $product->category->name;
}Verification:
# Enable query logging in tinker
php artisan tinkerDB::enableQueryLog();
$products = Product::with('category')->get();
dd(DB::getQueryLog()); // Should show 2 queriesIndexing Strategy
Add Indexes for:
- Foreign keys (
category_id,user_id) - Frequently filtered columns (
status,type) - Unique constraints (
email,sku) - Composite indexes for multi-column filters
Example Migration:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained()->onDelete('cascade');
$table->string('sku')->unique();
$table->string('name');
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
$table->decimal('price', 10, 2);
$table->timestamps();
// Indexes
$table->index('category_id'); // Foreign key
$table->index('status'); // Frequently filtered
$table->index(['category_id', 'status']); // Composite for common query
$table->index('created_at'); // For sorting/pagination
});Verify Index Usage:
-- PostgreSQL
EXPLAIN ANALYZE SELECT * FROM products WHERE category_id = 1 AND status = 'published';
-- MySQL
EXPLAIN SELECT * FROM products WHERE category_id = 1 AND status = 'published';Look for Index Scan (good) vs Seq Scan (bad).
Pagination for Large Datasets
Problem: OFFSET Performance Degradation
// ❌ Bad: OFFSET becomes slow with large offsets
Product::paginate(20); // Page 1000 = OFFSET 20000 (slow!)Solution: Cursor Pagination
// ✅ Good: Uses WHERE id > last_id (fast at any page)
Product::orderBy('id')->cursorPaginate(20);API Response:
{
"data": [...],
"meta": {
"per_page": 20,
"next_cursor": "eyJpZCI6MTAwMCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ",
"prev_cursor": null
}
}Database Connection Pooling
For High-Traffic Applications:
// config/database.php
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
'options' => [
PDO::ATTR_PERSISTENT => true, // Connection pooling
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
],
],Caching Strategies
Application Cache
Cache Expensive Operations:
use Illuminate\Support\Facades\Cache;
// ❌ Bad: Recalculate every request
$stats = [
'total_products' => Product::count(),
'total_orders' => Order::count(),
'revenue' => Order::sum('total'),
];
// ✅ Good: Cache for 1 hour
$stats = Cache::remember('dashboard.stats', 3600, function () {
return [
'total_products' => Product::count(),
'total_orders' => Order::count(),
'revenue' => Order::sum('total'),
];
});Invalidate Cache on Changes:
class Product extends Model
{
protected static function booted()
{
static::saved(function () {
Cache::forget('dashboard.stats');
});
static::deleted(function () {
Cache::forget('dashboard.stats');
});
}
}Query Result Cache
For Rarely-Changing Data:
// Cache categories for 24 hours
$categories = Cache::remember('categories.all', 86400, function () {
return Category::with('children')->get();
});Cache Tags (Redis/Memcached only):
// Cache with tags for selective invalidation
Cache::tags(['products', 'category:' . $categoryId])->remember(
"products.category.{$categoryId}",
3600,
fn() => Product::where('category_id', $categoryId)->get()
);
// Invalidate all product caches
Cache::tags('products')->flush();
// Invalidate specific category
Cache::tags('category:5')->flush();View Cache
Cache Rendered Views:
// In controller
public function index()
{
$products = Cache::remember('products.index', 3600, function () {
return Product::with('category')->paginate(20);
});
return view('products.index', compact('products'));
}Cache Entire Response:
// routes/web.php
Route::get('/products', [ProductController::class, 'index'])
->middleware('cache.response:3600'); // Custom middlewareCustom Cache Response Middleware:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Cache;
class CacheResponse
{
public function handle($request, Closure $next, $ttl = 3600)
{
if ($request->method() !== 'GET') {
return $next($request);
}
$key = 'response:' . md5($request->fullUrl());
return Cache::remember($key, $ttl, function () use ($request, $next) {
return $next($request);
});
}
}Asset Optimization
Vite Build Optimization
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
// Split vendor code
vendor: ['alpinejs', 'axios'],
preline: ['preline'],
},
},
},
chunkSizeWarningLimit: 1000,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
},
},
},
});Build for Production:
npm run build
# Verify output
ls -lh public/build/assets/
# Should show minified .js and .css filesImage Optimization
Use Laravel Intervention Image:
composer require intervention/imageuse Intervention\Image\Facades\Image;
// Resize and optimize on upload
$image = Image::make($request->file('photo'))
->resize(800, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
})
->encode('webp', 80); // Convert to WebP, 80% quality
$path = 'products/' . uniqid() . '.webp';
Storage::disk('public')->put($path, $image);Serve Optimized Images:
<img
src="{{ Storage::url($product->image) }}"
alt="{{ $product->name }}"
loading="lazy"
width="800"
height="600"
>CDN Integration
// config/filesystems.php
'disks' => [
'cdn' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'), // CloudFront URL
],
],// Upload to CDN
Storage::disk('cdn')->put('products/image.jpg', $imageContent);
// Get CDN URL
$url = Storage::disk('cdn')->url('products/image.jpg');
// https://cdn.example.com/products/image.jpgLaravel Octane
Installation
composer require laravel/octane
# Choose Swoole or RoadRunner
php artisan octane:install --server=swooleConfiguration
// config/octane.php
return [
'server' => env('OCTANE_SERVER', 'swoole'),
'swoole' => [
'options' => [
'http_compression' => true,
'http_compression_level' => 6,
'compression_min_length' => 20,
'open_http2_protocol' => true,
],
],
'warm' => [
...['config', 'routes', 'views'],
],
'cache' => [
'rows' => 1000,
'bytes' => 10000,
],
];Running Octane
# Development
php artisan octane:start --watch
# Production
php artisan octane:start --server=swoole --host=0.0.0.0 --port=8000 --workers=4Octane-Safe Code
❌ Avoid:
// Static properties persist across requests
class ProductController
{
protected static $cache = []; // ❌ Shared across requests!
}✅ Use:
// Request-scoped data
class ProductController
{
public function index(Request $request)
{
$cache = []; // ✅ Fresh per request
}
}FrankenPHP
Installation
# Download FrankenPHP
curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 -o frankenphp
chmod +x frankenphp
# Run
./frankenphp php-server --listen :8000Caddyfile Configuration
{
frankenphp
order php_server before file_server
}
:8000 {
root * public
php_server
encode gzip
file_server
}Worker Mode
// public/index.php (FrankenPHP worker mode)
<?php
use Illuminate\Http\Request;
require __DIR__.'/../vendor/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$handler = function () use ($app) {
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$request = Request::capture();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
$app->flush(); // Clear request-scoped bindings
};
// Worker mode: handle multiple requests in same process
for ($nbRequests = 0, $running = true; $running; ++$nbRequests) {
$running = \frankenphp_handle_request($handler);
gc_collect_cycles();
}Monitoring and Profiling
Laravel Telescope (Development)
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrateAccess: http://localhost:8000/telescope
Monitor:
- Slow queries (> 100ms)
- N+1 query detection
- Request duration
- Memory usage
Laravel Debugbar (Development)
composer require barryvdh/laravel-debugbar --devShows in browser:
- Query count and duration
- View rendering time
- Memory usage
- Route information
Production APM
Laravel Pulse:
composer require laravel/pulse
php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"
php artisan migrateAccess: http://localhost:8000/pulse
New Relic Integration:
composer require newrelic/newrelic-php-agent// config/logging.php
'channels' => [
'newrelic' => [
'driver' => 'monolog',
'handler' => NewRelic\Monolog\Enricher\Handler::class,
],
],Performance Checklist
Development
- [ ] Enable query logging to detect N+1 problems
- [ ] Use Telescope/Debugbar to profile requests
- [ ] Test with realistic data volumes
- [ ] Profile memory usage with large datasets
Database
- [ ] Add indexes for foreign keys and filtered columns
- [ ] Use eager loading for relationships
- [ ] Implement cursor pagination for large lists
- [ ] Enable query cache for read-heavy tables
Caching
- [ ] Cache expensive calculations (> 100ms)
- [ ] Cache rarely-changing data (categories, settings)
- [ ] Implement cache invalidation strategy
- [ ] Use Redis/Memcached for production
Assets
- [ ] Minify CSS/JS with Vite
- [ ] Optimize images (WebP, lazy loading)
- [ ] Use CDN for static assets
- [ ] Enable gzip/brotli compression
Server
- [ ] Use Octane or FrankenPHP for production
- [ ] Enable opcache (
opcache.enable=1) - [ ] Configure PHP memory limit (
memory_limit=256M) - [ ] Use HTTP/2 and compression
Monitoring
- [ ] Set up APM (Pulse, New Relic, Datadog)
- [ ] Monitor slow queries (> 100ms)
- [ ] Track error rates and response times
- [ ] Set up alerts for performance degradation
Benchmarking
Apache Bench
# Test homepage performance
ab -n 1000 -c 10 http://localhost:8000/
# Results:
# Requests per second: 500 [#/sec]
# Time per request: 20 [ms] (mean)Laravel Benchmarking
use Illuminate\Support\Benchmark;
Benchmark::dd([
'Scenario 1' => fn () => Product::all(),
'Scenario 2' => fn () => Product::with('category')->get(),
'Scenario 3' => fn () => Cache::remember('products', 60, fn () => Product::all()),
], iterations: 100);
// Output:
// Scenario 1: 150ms
// Scenario 2: 80ms (eager loading wins)
// Scenario 3: 5ms (cache wins)Conclusion
Performance optimization is iterative:
- Measure with profiling tools
- Identify bottlenecks (queries, cache misses)
- Optimize the slowest parts first
- Verify improvements with benchmarks
- Monitor in production
Focus on the 20% of optimizations that give 80% of the gains:
- Fix N+1 queries
- Add database indexes
- Cache expensive operations
- Use Octane/FrankenPHP
- Optimize assets
Next Steps:
- Set up Telescope:
/testing/browser-testing - Configure caching:
/reference/configuration - Deploy with Octane:
/getting-started/installation