Reference

Comprehensive guide to optimizing Laravolt v8 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

Laravolt under Octane

Laravolt is not installed with Octane (since v8.0). Require laravel/octane yourself, and use laravolt/laravolt 8.0.1 or later. Older versions rebuilt the sidebar menu on every request inside a long-running worker, so latency grew with every request. In production they also cached one user's menu for everyone, which made every page after the first return 500 on Laravel 13 with a file, database or redis cache.

What to keep in mind:

  • Register menu items with app('laravolt.menu.builder')->register(...) or config/laravolt/menu/*.php in a service provider's boot(). They are registered once per worker, and the menu is rebuilt for each request from those registrations, filtered by the current user's permissions.
  • Do not add items to app('laravolt.menu.sidebar') from a request (a controller or middleware). The sidebar singleton lives as long as the worker.
  • For a complete, measured example — Octane on FrankenPHP in worker mode, Docker Compose, SQLite tuned for concurrency — see laravolt/voltprocure.

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: /testing/browser-testing
  • Configure caching: /reference/configuration
  • Deploy with Octane: /getting-started/installation
Previous
Configuration