Reference
Performance Optimization
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 tinkerphp 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:
-- PostgreSQLEXPLAIN ANALYZE SELECT * FROM products WHERE category_id = 1 AND status = 'published';-- MySQLEXPLAIN 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 offsetsProduct::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 invalidationCache::tags(['products', 'category:' . $categoryId])->remember( "products.category.{$categoryId}", 3600, fn() => Product::where('category_id', $categoryId)->get());// Invalidate all product cachesCache::tags('products')->flush();// Invalidate specific categoryCache::tags('category:5')->flush();View Cache
Cache Rendered Views:
// In controllerpublic 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.phpRoute::get('/products', [ProductController::class, 'index']) ->middleware('cache.response:3600'); // Custom middlewareCustom Cache Response Middleware:
<?phpnamespace 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.jsimport { 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 outputls -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 CDNStorage::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 RoadRunnerphp artisan octane:install --server=swooleConfiguration
// config/octane.phpreturn [ '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
# Developmentphp artisan octane:start --watch# Productionphp artisan octane:start --server=swoole --host=0.0.0.0 --port=8000 --workers=4Octane-Safe Code
❌ Avoid:
// Static properties persist across requestsclass ProductController{ protected static $cache = []; // ❌ Shared across requests!}✅ Use:
// Request-scoped dataclass ProductController{ public function index(Request $request) { $cache = []; // ✅ Fresh per request }}FrankenPHP
Installation
# Download FrankenPHPcurl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 -o frankenphpchmod +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)<?phpuse 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 processfor ($nbRequests = 0, $running = true; $running; ++$nbRequests) { $running = \frankenphp_handle_request($handler); gc_collect_cycles();}Monitoring and Profiling
Laravel Telescope (Development)
composer require laravel/telescope --devphp artisan telescope:installphp 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/pulsephp 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 performanceab -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:
/v7/testing/browser-testing - Configure caching:
/v7/reference/configuration - Deploy with Octane:
/v7/getting-started/installation