Ottimizzare un’applicazione PHP significa ridurre il tempo di risposta, migliorare il throughput e contenere l’uso di CPU e memoria, mantenendo il codice affidabile e manutenibile. In pratica si lavora su più livelli: runtime PHP e opcache, design del codice, I/O e database, caching, logging, rete e infrastruttura. Questo articolo raccoglie una checklist ragionata, con esempi e criteri pratici per misurare e intervenire senza “ottimizzazioni premature”.
1) Parti dai numeri: misurazione e profili
Prima di cambiare il codice, definisci una metrica (p95, p99, throughput, tempo di query, memoria) e misura in condizioni realistiche. La performance percepita spesso è dominata da poche “hot path”: un endpoint molto usato, una query lenta, un loop su molti record, un’operazione di serializzazione o I/O. Due regole:
- Riproduci il problema in modo deterministico (stesso input, stesso dataset, stesso carico).
- Ottimizza una cosa per volta e verifica con benchmark ripetibili.
In ambiente di sviluppo puoi aggiungere misurazioni leggere (timing e memoria) per individuare colli di bottiglia evidenti. Evita di lasciare misurazioni verbose in produzione, a meno che siano campionate e centralizzate.
<?php
$start = hrtime(true);
// ... codice da misurare ...
$elapsedMs = (hrtime(true) - $start) / 1e6;
$memNow = memory_get_usage(true);
$memPeak = memory_get_peak_usage(true);
error_log(sprintf(
"elapsed=%.2fms mem=%dKB peak=%dKB",
$elapsedMs,
(int) ($memNow / 1024),
(int) ($memPeak / 1024)
));
2) Abilita e verifica OPcache
OPcache è uno degli acceleratori più efficaci: conserva in memoria il bytecode compilato degli script PHP, evitando compilazioni ripetute a ogni richiesta. In produzione deve essere attivo e dimensionato correttamente. I problemi tipici sono:
- Memoria OPcache insufficiente: il sistema “scarta” script e ricompila spesso.
- Configurazioni di revalidate troppo conservative o troppo aggressive (in prod è comune disabilitare la validazione timestamp).
- Warmup insufficiente dopo deploy: prime richieste più lente.
Un controllo veloce lato applicazione per capire se OPcache è attivo:
<?php
if (function_exists('opcache_get_status')) {
$status = opcache_get_status(false);
if (!is_array($status) || empty($status['opcache_enabled'])) {
error_log("OPcache non attivo");
}
}
3) Riduci l’I/O: filesystem, rete, serializzazione
L’I/O è spesso più lento della CPU. Ogni accesso a filesystem, rete o servizi esterni può dominare i tempi di risposta. Interventi ad alto impatto includono:
- Evitare letture ripetute di file/config a ogni richiesta: precompila configurazioni e cache in memoria o in un cache store.
- Batching delle richieste verso servizi esterni, timeouts ragionevoli, retries controllati.
- Ridurre conversioni costose (JSON encode/decode, (de)serializzazione) dove possibile.
Un pattern comune è caricare più volte la stessa risorsa esterna nella stessa richiesta: centralizza l’accesso e memoizza in-request.
<?php
final class InRequestCache {
private array $cache = [];
public function remember(string $key, callable $compute): mixed {
if (!array_key_exists($key, $this->cache)) {
$this->cache[$key] = $compute();
}
return $this->cache[$key];
}
}
// Uso:
$irc = new InRequestCache();
$user = $irc->remember("user:$id", fn() => $repo->findUserById($id));
4) Database: indici, query, N+1 e connessioni
Nella maggior parte delle applicazioni web, il database è il primo sospettato. Ottimizzare “a caso” sul codice PHP senza guardare query e piani di esecuzione spesso porta a guadagni minimi. I punti chiave:
4.1 Evita N+1 e usa query aggregate o eager loading
Il problema N+1 accade quando recuperi una lista e poi fai una query per ogni elemento. È devastante con dataset medi o grandi. Spesso si risolve con JOIN mirate, IN (...) e query aggregate.
<?php
// Anti-pattern: N+1
$posts = $db->query("SELECT id, title FROM posts WHERE published = 1")->fetchAll();
foreach ($posts as $p) {
$comments = $db->prepare("SELECT COUNT(*) FROM comments WHERE post_id = ?");
$comments->execute([$p['id']]);
$p['comment_count'] = (int) $comments->fetchColumn();
}
// Pattern migliore: aggregazione
$sql = "
SELECT p.id, p.title, COALESCE(c.cnt, 0) AS comment_count
FROM posts p
LEFT JOIN (
SELECT post_id, COUNT(*) AS cnt
FROM comments
GROUP BY post_id
) c ON c.post_id = p.id
WHERE p.published = 1
";
$posts = $db->query($sql)->fetchAll();
4.2 Indici e piani di esecuzione
Se una query filtra per colonne senza indice, il database può scansionare molte righe. Gli indici vanno progettati sui pattern reali: WHERE, JOIN, ORDER BY. Usa EXPLAIN/EXPLAIN ANALYZE per verificare. Un indice “giusto” può ridurre tempi di ordini di grandezza.
4.3 Prepared statements e riduzione del parsing
I prepared statements aiutano sicurezza e, in alcuni casi, performance: il DB può riutilizzare piani e ridurre parsing ripetuto, soprattutto con query ripetitive. Mantieni connessioni e statement riutilizzabili quando l’architettura lo consente.
<?php
$stmt = $db->prepare("SELECT id, email FROM users WHERE status = ? AND created_at >= ?");
$stmt->execute([$status, $fromDate]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
5) Caching: strategia, livelli e invalidazione
Il caching è “moltiplicatore” di performance se applicato bene. La parte difficile non è mettere in cache, ma invalidare correttamente e prevenire dati incoerenti. Pensa a più livelli:
- Cache in-request (memoization) per evitare calcoli ripetuti.
- Cache applicativa (Redis/Memcached) per dati condivisi tra richieste.
- HTTP caching e CDN (Cache-Control, ETag) per contenuti statici o semi-statici.
Una tecnica utile è il “cache-aside”: l’app prova a leggere dal cache store, se manca calcola e salva. Usa TTL e versioning delle chiavi per semplificare invalidazioni.
<?php
function cacheKey(string $base, int $version, array $parts = []): string {
$suffix = $parts ? ':' . implode(':', $parts) : '';
return $base . ':v' . $version . $suffix;
}
// Esempio: caching di un profilo utente
$key = cacheKey('user_profile', 3, [$userId]);
$profileJson = $redis->get($key);
if ($profileJson === false) {
$profile = $repo->loadUserProfile($userId); // query e join necessarie
$profileJson = json_encode($profile, JSON_UNESCAPED_UNICODE);
$redis->setex($key, 300, $profileJson); // TTL 5 minuti
}
$profile = json_decode($profileJson, true, flags: JSON_THROW_ON_ERROR);
Attenzione al “thundering herd”: se molte richieste scadono insieme, tutte ricomputano. Soluzioni comuni: TTL con jitter, lock temporanei o “stale-while-revalidate” (servi dato vecchio per poco mentre rigeneri).
6) Algoritmi e strutture dati: l’ottimizzazione più pulita
Migliorare la complessità algoritmica spesso è più efficace di micro-ottimizzazioni. Alcuni esempi tipici in PHP:
- Evitare
in_arrayripetuti su array grandi: converti a set usando una mappa. - Preferire
foreachaarray_map/array_filterquando serve anche evitare allocazioni intermedie. - Ridurre copie di array: attenzione a funzioni che duplicano dati e a passaggi non necessari.
<?php
// Anti-pattern: O(n*m) con in_array su lista grande
$allowed = $repo->loadAllowedIds(); // migliaia
$result = [];
foreach ($items as $it) {
if (in_array($it['id'], $allowed, true)) {
$result[] = $it;
}
}
// Pattern migliore: set O(1) medio
$allowedSet = array_fill_keys($allowed, true);
$result = [];
foreach ($items as $it) {
if (isset($allowedSet[$it['id']])) {
$result[] = $it;
}
}
7) Gestione della memoria e garbage collection
In PHP, la memoria conta soprattutto su processi long-running (worker di queue, websocket, cron lunghi) e in script che gestiscono grandi volumi di dati (export, import, ETL). Suggerimenti pratici:
- Processa a stream o a chunk invece di caricare tutto in memoria.
- Evita strutture dati annidate enormi se non servono: serializza progressivamente.
- Nei worker, libera riferimenti a oggetti e array grandi una volta usati.
<?php
// Esempio: elaborazione a chunk per ridurre memoria
$offset = 0;
$limit = 1000;
while (true) {
$rows = $repo->fetchRows($offset, $limit);
if (!$rows) {
break;
}
foreach ($rows as $row) {
$processor->handle($row);
}
// libera riferimenti e avanza
unset($rows);
$offset += $limit;
}
8) Evita micro-ottimizzazioni inutili, ma cura gli hot spot
“Micro-ottimizzare” concatenazioni o piccoli dettagli raramente cambia il quadro, a meno che il punto sia eseguito milioni di volte. Concentrati su:
- riduzione del numero di chiamate a funzioni/servizi esterni in hot path;
- batching e caching;
- query e indici;
- algoritmi e strutture dati.
Un esempio frequente è fare parsing ripetuto di dati invarianti. Se un valore è costante per l’intero processo, calcolalo una volta sola e riutilizzalo.
<?php
final class Config {
private static ?array $cached = null;
public static function get(): array {
if (self::$cached === null) {
$path = __DIR__ . '/config.php';
self::$cached = require $path;
}
return self::$cached;
}
}
9) Framework e autoload: ottimizza senza perdere qualità
Molti progetti PHP usano Composer e un framework. Qui l’obiettivo è ridurre il lavoro “automatico” che non porta valore nella richiesta:
- Composer autoload ottimizzato in produzione (classmap autoritativa quando appropriato).
- Ridurre service provider/listener non necessari e lazy-load dei servizi pesanti.
- Cache della configurazione e delle route se il framework lo supporta.
In generale: più “magia” equivale a più lavoro a runtime. Usa la magia dove aumenta produttività, ma misura gli impatti su richieste critiche.
10) Logging, error handling e osservabilità
Logging e gestione errori influenzano la performance, specie se si logga troppo o si scrive su disco sincrono. Linee guida:
- Log strutturati e campionati per eventi ad alto volume.
- Evita stringhe enormi o serializzazioni profonde in log.
- Usa livelli corretti (debug in produzione solo se necessario e con sampling).
<?php
// Logging strutturato con contesto leggero
$logger->info('checkout_completed', [
'user_id' => $userId,
'order_id' => $orderId,
'elapsed_ms' => $elapsedMs,
]);
11) Ottimizzazioni lato HTTP: compressione, payload e caching
Anche se il server risponde veloce, payload grandi e assenza di caching possono rallentare l’esperienza. Le ottimizzazioni tipiche includono:
- Ridurre dimensioni JSON (campi necessari, paginazione, compressione a livello web server).
- ETag/Last-Modified quando il contenuto è riutilizzabile.
- Cache-Control con TTL adeguati e invalidazioni via versioning degli asset.
<?php
// Esempio: risposta JSON con header di caching
$data = ['items' => $items, 'next' => $nextCursor];
$body = json_encode($data, JSON_UNESCAPED_UNICODE);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: public, max-age=60'); // 60s
echo $body;
12) Checklist finale: priorità e percorso tipico
- Misura: definisci metriche e riproduci (p95/p99, query time, memoria).
- OPcache: attivo e dimensionato.
- Database: elimina N+1, aggiungi indici, riduci round-trip, verifica piani.
- Caching: applica cache-aside, TTL e versioning; proteggi da thundering herd.
- I/O: riduci chiamate a servizi e filesystem; batching e timeouts.
- Algoritmi: passa da O(n*m) a O(n) dove possibile; evita strutture inutili.
- Memoria: chunking/streaming; worker puliti senza leak.
- HTTP: riduci payload, usa caching e compressione.
Se applichi questi passi nell’ordine giusto, di solito ottieni miglioramenti significativi senza compromettere chiarezza e qualità. La chiave è sempre la stessa: misurare, cambiare poco e verificare.