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
// ❌ 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=4Laravolt 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(...)orconfig/laravolt/menu/*.phpin a service provider'sboot(). 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:
// 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