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

PHP
// ❌ 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

PHP
// ✅ Good: 2 queries total (1 for products, 1 for categories)
$products = Product::with('category')->get();
foreach ($products as $product) {
echo $product->category->name;
}

Verification:

Bash
# Enable query logging in tinker
php artisan tinker
PHP
DB::enableQueryLog();
$products = Product::with('category')->get();
dd(DB::getQueryLog()); // Should show 2 queries

Indexing 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:

PHP
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:

SQL
-- 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

PHP
// ❌ Bad: OFFSET becomes slow with large offsets
Product::paginate(20); // Page 1000 = OFFSET 20000 (slow!)

Solution: Cursor Pagination

PHP
// ✅ Good: Uses WHERE id > last_id (fast at any page)
Product::orderBy('id')->cursorPaginate(20);

API Response:

JSON
{
"data": [...],
"meta": {
"per_page": 20,
"next_cursor": "eyJpZCI6MTAwMCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ",
"prev_cursor": null
}
}

Database Connection Pooling

For High-Traffic Applications:

PHP
// 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:

PHP
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:

PHP
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:

PHP
// Cache categories for 24 hours
$categories = Cache::remember('categories.all', 86400, function () {
return Category::with('children')->get();
});

Cache Tags (Redis/Memcached only):

PHP
// 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:

PHP
// 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:

PHP
// routes/web.php
Route::get('/products', [ProductController::class, 'index'])
->middleware('cache.response:3600'); // Custom middleware

Custom Cache Response Middleware:

PHP
<?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

JavaScript
// 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:

Bash
npm run build
# Verify output
ls -lh public/build/assets/
# Should show minified .js and .css files

Image Optimization

Use Laravel Intervention Image:

Bash
composer require intervention/image
PHP
use 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:

Blade
<img
src="{{ Storage::url($product->image) }}"
alt="{{ $product->name }}"
loading="lazy"
width="800"
height="600"
>

CDN Integration

PHP
// 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
],
],
PHP
// 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.jpg

Laravel Octane

Installation

Bash
composer require laravel/octane
# Choose Swoole or RoadRunner
php artisan octane:install --server=swoole

Configuration

PHP
// 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

Bash
# Development
php artisan octane:start --watch
# Production
php artisan octane:start --server=swoole --host=0.0.0.0 --port=8000 --workers=4

Octane-Safe Code

❌ Avoid:

PHP
// Static properties persist across requests
class ProductController
{
protected static $cache = []; // ❌ Shared across requests!
}

✅ Use:

PHP
// Request-scoped data
class ProductController
{
public function index(Request $request)
{
$cache = []; // ✅ Fresh per request
}
}

FrankenPHP

Installation

Bash
# 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 :8000

Caddyfile Configuration

Caddyfile
{
frankenphp
order php_server before file_server
}
:8000 {
root * public
php_server
encode gzip
file_server
}

Worker Mode

PHP
// 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)

Bash
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate

Access: http://localhost:8000/telescope

Monitor:

  • Slow queries (> 100ms)
  • N+1 query detection
  • Request duration
  • Memory usage

Laravel Debugbar (Development)

Bash
composer require barryvdh/laravel-debugbar --dev

Shows in browser:

  • Query count and duration
  • View rendering time
  • Memory usage
  • Route information

Production APM

Laravel Pulse:

Bash
composer require laravel/pulse
php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"
php artisan migrate

Access: http://localhost:8000/pulse

New Relic Integration:

Bash
composer require newrelic/newrelic-php-agent
PHP
// 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

Bash
# 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

PHP
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:

  1. Measure with profiling tools
  2. Identify bottlenecks (queries, cache misses)
  3. Optimize the slowest parts first
  4. Verify improvements with benchmarks
  5. 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
Previous
Configuration