Ottimizzare un’applicazione Laravel non significa solo “renderla più veloce”: vuol dire ridurre latenza, consumo di CPU e I/O, migliorare la prevedibilità sotto carico e abbassare i costi operativi. In questa guida trovi un percorso completo, dall’osservabilità alle ottimizzazioni applicative e infrastrutturali, con esempi concreti e checklist operative.
1. Misura prima di ottimizzare: osservabilità e baseline
Prima di intervenire, definisci una baseline riproducibile (stesso dataset, stesso carico, stessi endpoint) e misura con strumenti affidabili. Senza misure rischi di ottimizzare “a sensazione” e spostare i colli di bottiglia altrove.
Strumenti consigliati
- Laravel Telescope (dev/staging): query, request, job, cache, eventi.
- Laravel Debugbar (dev): panoramica immediata su query e view.
- APM (production): New Relic, Datadog, Elastic APM, ecc. per tracce distribuite, SQL slow, error rate.
- Database slow query log e EXPLAIN per analizzare piani di esecuzione.
- Load testing: k6, JMeter, Locust, wrk, vegeta.
Metriche da tenere d’occhio
- p50/p95/p99 (latenza), non solo media.
- Throughput (req/s) e error rate.
- Numero di query e tempo totale SQL per request.
- Cache hit rate, latenza di Redis/Memcached.
- CPU/memoria di PHP-FPM e del database.
2. Caching: la leva più potente (se usata bene)
Il caching riduce lavoro ripetitivo e sposta tempo da CPU/DB a memoria. Laravel offre più livelli: config/route/view cache, cache applicativa, cache HTTP e cache dei risultati SQL indirettamente via query cache applicativa.
2.1 Cache di configurazione, rotte e view
In produzione, abilita sempre le cache “build-time”.
php artisan config:cache
php artisan route:cache
php artisan view:cache
Nota: route:cache richiede che tutte le route siano serializzabili (niente closure nelle definizioni).
2.2 Cache applicativa con TTL e invalidazione
Usa Cache::remember() per memorizzare risultati costosi. Scegli una strategia di invalidazione: TTL breve, cache tagging (se supportato), o invalidazione esplicita su eventi di dominio.
use Illuminate\Support\Facades\Cache;
$stats = Cache::remember(
"dashboard:stats:user:{$user->id}",
now()->addMinutes(10),
function () use ($user) {
return [
'orders' => $user->orders()->count(),
'spent' => $user->orders()->sum('total'),
];
}
);
Se usi Redis, valuta i tag per invalidare gruppi correlati (attenzione ai driver che non li supportano).
Cache::tags(['dashboard', "user:{$user->id}"])
->remember('stats', 600, fn () => $this->computeStats($user));
// In invalidazione:
Cache::tags(['dashboard', "user:{$user->id}"])->flush();
2.3 Cache delle view e dei fragment
Per pagine con parti statiche o semi-statiche, usa cache a livello di view per blocchi specifici.
@php
$key = "home:featured:v1";
@endphp
{!! Cache::remember($key, now()->addMinutes(30), function () {
return view('partials.featured', ['items' => \App\Models\Item::featured()->limit(8)->get()])->render();
}) !!}
2.4 HTTP caching e CDN
Se servi contenuti pubblici, sfrutta Cache-Control, ETag o Last-Modified e metti una CDN davanti. Anche senza “full page cache”, una CDN riduce RTT e assorbe picchi.
3. Database: query efficienti, indici corretti, meno round-trip
Nella maggior parte dei casi, i veri colli di bottiglia sono nel database: query N+1, indici mancanti, join pesanti, aggregazioni su colonne non indicizzate, e troppi round-trip per request.
3.1 Elimina il problema N+1
Usa eager loading (with, load) e carica solo ciò che serve.
// Male: N+1 su author
$posts = Post::latest()->take(20)->get();
foreach ($posts as $post) {
echo $post->author->name;
}
// Bene: eager loading
$posts = Post::with('author:id,name')
->latest()
->take(20)
->get();
foreach ($posts as $post) {
echo $post->author->name;
}
3.2 Seleziona solo le colonne necessarie
Riduci I/O e memoria selezionando colonne specifiche, soprattutto su tabelle “wide”.
$users = User::query()
->select(['id', 'name', 'email'])
->where('active', true)
->orderBy('id', 'desc')
->paginate(50);
3.3 Indici: progettazione e verifica
Un indice giusto può trasformare una query da secondi a millisecondi. Regole pratiche:
- Indicizza colonne usate in
WHERE,JOIN,ORDER BY,GROUP BY. - Preferisci indici compositi che rispecchiano l’ordine dei filtri più selettivi.
- Evita indici inutili: rallentano scritture e occupano spazio.
// Esempio migration con indice composito
Schema::table('orders', function (Blueprint $table) {
$table->index(['user_id', 'status', 'created_at']);
});
Verifica sempre con EXPLAIN e log slow query. Se l’ottimizzatore non usa l’indice, controlla: cardinalità, tipo colonna, funzioni sul campo (es. DATE(created_at)), confronti con tipi diversi.
3.4 Aggregazioni e report: precomputazione e tabelle di supporto
Se calcoli spesso statistiche (es. dashboard), valuta:
- Materializzare risultati in tabelle aggregate aggiornate via job.
- Usare code/queue per ricalcoli asincroni.
- Cache dei risultati per range temporali.
3.5 Pagination efficiente: keyset pagination
Per dataset grandi, OFFSET diventa costoso. Usa keyset pagination quando possibile.
$orders = Order::query()
->where('user_id', $userId)
->where('id', '<', $cursorId)
->orderByDesc('id')
->limit(50)
->get();
4. Eloquent e dominio: performance senza perdere leggibilità
Eloquent è comodo ma può generare query subottimali se usato senza attenzione. L’obiettivo non è “abbandonarlo”, ma usarlo con consapevolezza.
4.1 Evita accessors/mutators costosi in loop
Accessors che fanno query o calcoli pesanti possono moltiplicare i costi. Sposta logica in query (con aggregazioni) o pre-carica dati.
4.2 Usa chunk e cursor per grandi dataset
Per elaborazioni batch, evita di caricare tutto in memoria.
User::query()
->where('active', true)
->chunkById(1000, function ($users) {
foreach ($users as $user) {
// Processa
}
});
// Streaming (minimizza memoria), attenzione a connessioni lunghe
foreach (User::where('active', true)->cursor() as $user) {
// Processa
}
4.3 Mass update/insert e upsert
Quando devi aggiornare molte righe, preferisci query set-based e upsert per ridurre round-trip.
DB::table('inventory')->upsert(
[
['sku' => 'A1', 'qty' => 10],
['sku' => 'B2', 'qty' => 5],
],
['sku'],
['qty']
);
5. Code e background jobs: sposta lavoro fuori dalla request
La request HTTP dovrebbe fare il minimo necessario per rispondere. Tutto ciò che può essere asincrono va in coda: invio email, generazione report, elaborazioni immagini, chiamate a servizi esterni non critiche.
5.1 Pattern: “rispondi subito, completa dopo”
// Controller
dispatch(new \App\Jobs\GenerateReport($user->id));
return response()->json([
'status' => 'accepted',
'message' => 'Report in generazione'
], 202);
5.2 Configurazione e tuning dei worker
- Separare code per tipologia (es.
emails,reports,default). - Impostare
timeout,tries, backoff e rate limiting. - Monitorare throughput, fallimenti e tempi di attesa (queue latency).
6. Eventi, listeners e notifiche: attenzione agli effetti collaterali
Eventi e listener sono ottimi per decoupling, ma possono introdurre lavoro nascosto dentro la request. Se un listener è pesante, rendilo queued.
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class SendWelcomeEmail implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
public function handle(UserRegistered $event): void
{
// invio email o chiamata esterna
}
}
7. PHP runtime e server: OPcache, PHP-FPM, e deployment
7.1 OPcache: indispensabile
OPcache riduce drasticamente il costo di parsing/compilazione PHP. In produzione deve essere attivo e correttamente dimensionato (memoria, max accelerated files, validate timestamps). In ambienti con deploy atomici, puoi disabilitare la validazione dei timestamp e fare reset su deploy.
7.2 PHP-FPM: processi, pm e timeouts
Bilancia pm.max_children in base a RAM disponibile e consumo medio per processo. Troppi children causano swap e peggiorano tutto; troppo pochi aumentano la coda in attesa. Misura memoria reale con carico tipico.
7.3 Autoloader ottimizzato e dependency trimming
composer install --no-dev --optimize-autoloader --classmap-authoritative
Rimuovi dipendenze inutili e servizi caricati senza motivo. Il “peso” del container e dei provider impatta il tempo di bootstrap.
8. Laravel Octane e Swoole/RoadRunner: quando ha senso
Octane può ridurre la latenza eliminando parte del bootstrap per request mantenendo l’app “warm”. Non è una bacchetta magica: richiede disciplina per evitare state leakage (stato condiviso tra richieste) e attenzione a servizi non “stateless”. È particolarmente utile per applicazioni con bootstrap pesante e molte request al secondo.
Checklist Octane
- Evita singletons con stato mutabile non resettato.
- Controlla cache in-memory e variabili statiche.
- Usa test di carico e confronta p95/p99 prima/dopo.
9. Ottimizzazione delle view Blade e asset front-end
- Usa
view:cachein produzione. - Evita logica pesante nelle view: prepara i dati nel controller/service.
- Minimizza asset (Vite), abilita HTTP/2 o HTTP/3 dove possibile.
- Imposta caching aggressivo per asset versionati.
10. Riduci il numero di chiamate esterne e rendile resilienti
Le integrazioni (pagamenti, CRM, API terze) spesso dominano la latenza. Strategie:
- Timeout brevi e retry controllati (con backoff).
- Circuit breaker e fallback se il servizio è down.
- Caching delle risposte quando possibile.
- Spostare chiamate non critiche in coda.
use Illuminate\Support\Facades\Http;
$response = Http::timeout(2)
->retry(2, 200) // 2 retry, 200ms
->get($url);
if ($response->failed()) {
// fallback/circuit breaker (es. cache, valore di default, ecc.)
}
11. Logging e debug in produzione: meno rumore, più segnale
Log eccessivi o troppo “verbatim” possono diventare un collo di bottiglia, specie su storage lento o su pipeline di log ingest. Imposta livelli adeguati, evita log in loop, e usa correlazione con request-id per tracciare problemi senza duplicare troppo.
12. Sicurezza e performance: rate limiting e protezioni mirate
Un attacco (anche involontario) può saturare risorse. Usa rate limiting per endpoint sensibili e proteggi operazioni costose. Laravel offre middleware e limitatori configurabili.
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('login', function ($request) {
return Limit::perMinute(10)->by($request->ip());
});
13. Checklist finale per un piano di ottimizzazione
- Definisci baseline e metriche (p95, error rate, query count).
- Identifica i top endpoint per traffico e latenza (APM).
- Elimina N+1, riduci colonne, aggiungi indici e verifica con EXPLAIN.
- Attiva config/route/view cache e autoloader ottimizzato.
- Implementa cache applicativa con invalidazione chiara.
- Sposta lavoro pesante in queue; misura queue latency e throughput.
- Tuning OPcache e PHP-FPM; rivedi timeouts e limiti.
- Ottimizza chiamate esterne (timeout, retry, caching, fallback).
- Test di carico e regressione: confronta p50/p95/p99 prima/dopo.
- Itera: ottimizzazione è un ciclo, non un’azione una tantum.
Conclusione
La performance in Laravel si migliora con un approccio sistematico: misura, individua il collo di bottiglia dominante, applica cambiamenti mirati e verifica l’impatto sulle metriche che contano (p95/p99, error rate, saturazione risorse). Con caching ben progettato, query ottimizzate e lavoro asincrono, la maggior parte delle applicazioni ottiene miglioramenti significativi senza sacrificare la qualità del codice.