Novità in Symfony 8.2: miglioramenti delle prestazioni (Symfony Blog)
In Symfony siamo ossessionati dalle prestazioni, e Symfony 8.2 ne offre moltissimi esempi. Nelle ultime settimane, Nicolas Grekas ha profilato l'applicazione Symfony Demo e un progetto API personalizzato, trasformando ogni risultato in una pull request.
Il risultato sono pagine web, API, comandi console e build della cache più veloci, in produzione e in sviluppo.
Le API che serializzano e deserializzano molti oggetti ottengono alcuni dei maggiori vantaggi. Durante la normalizzazione degli oggetti, il Serializer calcola il contesto di ogni attributo una sola volta, evita di copiare l'array normalizzato per ogni attributo e recupera la mappatura del discriminatore una volta per oggetto invece che una volta per attributo. Su un'applicazione API che restituisce elenchi da 150 a 900 oggetti con gruppi di serializzazione e un name converter, le route di elenco sono dal 14% al 24% più veloci (PR #66383).
Leggere una proprietà con PropertyAccess ora richiede il 40% in meno di istruzioni PHP, quindi quelle route di elenco eseguono un altro 6%-8% in meno di istruzioni (PR #66397). Anche il name converter snake_case memorizza i propri risultati invece di eseguire un'espressione regolare per ogni attributo di ogni oggetto, il che rende le route di elenco dal 2% al 5% più veloci (PR #66384).
Anche la denormalizzazione (ad es. quando si usa #[MapRequestPayload]) è diventata più veloce:
- L'accessor delle proprietà viene creato solo per le classi che usano
#[SerializedPath], invece che una volta per ogni oggetto denormalizzato (PR #66398); - I parametri del costruttore vengono verificati prima di chiamare PropertyInfo, il che evita alcune ricerche durante la denormalizzazione di DTO readonly. Le richieste POST eseguono dal 4% al 6% in meno di istruzioni PHP (PR #66399);
- I file delle classi non vengono più letti a ogni richiesta per risolvere i tipi dei parametri del costruttore (PR #66400);
- Le property info delle classi mappate dal Serializer e dal Validator vengono ora pre-caricate in un unico file di cache invece di essere calcolate e memorizzate una voce alla volta. Le richieste POST sono dal 4% al 5% più veloci in produzione (PR #66386).
Anche PropertyInfo e TypeInfo sono stati ottimizzati. I docblock dei costruttori vengono ora analizzati una volta per classe invece che una volta per proprietà. Poiché le property info non vengono messe in cache in modalità debug, questo rende le richieste POST in dev dal 24% al 38% più veloci (da 10 a 14 ms ciascuna) (PR #66382). Anche i docblock delle classi vengono analizzati una volta per contesto di tipo invece che cinque o sei volte (PR #66401).
Altri miglioramenti:
- Il Validator riutilizza l'
IntlDateFormatterusato per formattare le date nei messaggi di violazione dei vincoli, invece di crearne uno nuovo ogni volta (PR #66396); - Il
ReflectionExtractorcrea il proprio inflector e type resolver solo quando servono, quindi una richiesta che serializza un oggetto carica 10 classi in meno (PR #66402); - Le eccezioni vengono registrate nei log senza prima trasformarle in oggetti
FlattenException. Registrare una risposta 422 ora richiede 70 µs invece di 125 µs (PR #66392).
Infine, gli errori del client (risposte HTTP 4xx) vengono ora registrati a livello warning invece che error (PR #66391). Questo riflette meglio un errore del client e fa anche risparmiare lavoro: con la ricetta Monolog predefinita (fingers_crossed con action_level: error), le risposte 400, 403 e 422 non svuotano più il buffer dei log della richiesta. Una risposta 422 generata da #[MapRequestPayload] è ora dal 3% al 4% più veloce.
Nota
Questa modifica cambia il livello di alcuni messaggi di log. Se ti affidi a essi (ad es. negli alert), consulta la PR #66391 per i dettagli. Puoi comunque configurare il livello di log per eccezione usando framework.exceptions o l'attributo #[WithLogLevel].
I vantaggi vanno oltre le API:
- Il
PhpFilesAdapterdel componente Cache non tenta più di includere file che non esistono. Un include fallito costa circa 10 µs, mentre verificare l'esistenza del file ne richiede 1 µs. Sul Symfony Demo con una directory di cache in sola lettura e senza APCu, questo fa risparmiare 830 µs per richiesta (PR #66347); ChainAdapter::get()restituisce direttamente gli hit del suo primo adapter, senza creare ogni volta due closure (PR #66346);- La modalità runtime non viene più risolta sulle richieste web, dove non è necessaria. Questo elimina 8 delle 18 ricerche di variabili d'ambiente di una richiesta in produzione nel Symfony Demo (PR #66367);
- L'HtmlSanitizer non analizza più gli input che non contengono
<o&. Sanificare una breve stringa di testo semplice (molto comune quando si usa l'opzionesanitize_htmlnei campi dei form) ora è 5 volte più economico e non alloca più 1,1 MB di memoria a ogni chiamata (PR #66389).
Trascorri gran parte della giornata nell'ambiente di sviluppo, quindi anche questo merita un po' di attenzione. I data collector Config, Security, Request e Cache del profiler ora rimandano il proprio lavoro statico (versioni, bundle, gerarchia dei ruoli, comandi cURL, ecc.) fino al salvataggio dei profili. Con PHP-FPM, ciò avviene dopo l'invio della risposta, facendo risparmiare fino a 0,6 ms per collector per pagina (PR #66365, PR #66371, PR #66372, PR #66364).
Anche il data collector del Serializer aggrega le chiamate annidate man mano che avvengono, invece di memorizzarle tutte fino alla fine. Questo fa risparmiare 5 MB di memoria su una richiesta in dev che serializza 1.000 ordini (PR #66390).
Se usi AssetMapper:
- Servire un asset con il dev server non costruisce più il profiler e Twig. Sul Symfony Demo, ogni asset viene servito in 6,3 ms invece di 7,0 ms (PR #66373);
- Ogni asset in cache viene caricato una volta per richiesta e da un unico file, invece di leggere e deserializzare due file ogni volta che serve. Il rendering dell'importmap del Symfony Demo in dev ora richiede 2,4 ms invece di 4,7 ms (PR #66369).
Build più veloci aiutano a ogni deploy, quando esegui cache:clear e cache:warmup, e in dev, ogni volta che una modifica di configurazione ricostruisce il container:
- I file di traduzione XLIFF vengono prima validati rispetto a uno schema molto più piccolo che si compila 10 volte più rapidamente (lo schema completo viene comunque usato quando serve). Sul Symfony Demo, un
cache:warmupa freddo passa da 1,77 s a 1,57 s e una richiesta in dev dopo la modifica di un file di traduzione passa da 82 ms a 67 ms (PR #66375); - Il cache warmer di Twig compila solo i temi dei form che la tua app può usare, invece di tutti i 13 temi inclusi in Symfony. Sul Symfony Demo, ciò significa 10 temi in meno e un cache warmup a freddo che richiede 2,11 s invece di 2,27 s (PR #66381). Anche i template elencati due volte con nomi diversi vengono compilati una sola volta (PR #66370);
- Il dump del container per il debug clona solo le definizioni di servizio che contengono variabili d'ambiente (22 su 1.344 nel Symfony Demo), invece di tutte. Questo passaggio passa da 49 ms a 14 ms (PR #66343);
- Il
XmlDumperevita la codifica dei valori ASCII semplici, che rappresentava quasi il 30% del tempo di dump (PR #66379); - Gli alberi di configurazione usati per generare i file
reference.phpeschema.jsonvengono ora costruiti una volta invece di due (PR #66378).
Symfony 8.2 inoltre termina la risposta prima che il profiler salvi i suoi dati, facendo concludere le pagine in dev da 11 a 13 ms prima con PHP-FPM (PR #66330). Anche la modifica di un file di traduzione non ricostruisce più il container, riducendo la richiesta successiva da circa 790 ms a 85 ms (PR #66332).
Questo lavoro ha migliorato anche altri progetti: Twig attraversa i nodi più rapidamente (Twig PR #4976) e TwigComponent pre-analizza i template più rapidamente (Symfony UX PR #3934). Ha persino raggiunto PHP stesso e Lexbor, il parser HTML5 alla base di Dom\HTMLDocument di PHP:
new ArrayIterator()enew ArrayObject(), che i nodi Twig, i bag di Symfony e le collection di Doctrine creano nei loro metodigetIterator(), ora eseguono circa 180 istruzioni in meno per chiamata (PHP PR #23946);- Lexbor non alloca più 8 volte la memoria di cui ha bisogno per alcuni dei suoi array interni, risparmiando circa 10 KB per parsing (Lexbor PR #428).
Alcune altre proposte sono ancora in fase di revisione:
htmlspecialchars()restituisce il proprio input quando non c'è nulla da codificare, che è il caso più comune nei template. Sul Symfony Demo, che la chiama 711 volte per richiesta, diventa circa 3,5 volte più economica (PHP PR #23957). Verificare prima l'UTF-8 quando si risolve il charset la rende un altro 40% più economica sulle stringhe brevi (PHP PR #23961);ArrayObjecteArrayIteratorcondividono gli array forniti (copy-on-write) invece di copiarli, quindi unforeachsu queste collection non duplica più i loro elementi (PHP PR #23945). Anche creare un iteratore mentre altri sono attivi (ad es. in una visita ricorsiva dei nodi Twig) è più veloce (PHP PR #23947);- Lexbor prealloca meno memoria, quindi il parsing di un piccolo documento richiede circa 200 KB invece di 1 MB (Lexbor PR #427).
Alcune delle modifiche menzionate sopra fanno risparmiare 70 microsecondi per richiesta (ovvero 2.000 volte più veloce di un battito di ciglia umano medio). Perché preoccuparsene? Perché in un progetto maturo come Symfony non puoi trovare una singola modifica magica che renda tutto due volte più veloce. Ecco perché continuiamo a renderlo più veloce, un piccolo miglioramento alla volta.
Aggiorna a Symfony 8.2 quando sarà rilasciata a novembre per ottenere tutto questo senza modificare una riga della tua applicazione. Se il profiling della tua applicazione rivela qualcosa di lento in Symfony, faccelo sapere. Ci importa di ogni microsecondo.
Altri articoli recenti
Laravel AI 1.1 e 1.2: Agent Skills, Cohere e structured output più solidi
Le release 1.1.0 e 1.2.0 di laravel/ai portano gli Agent Skills, l'approvazione obbligatoria per i tool built-in e nuovi provider come Cohere e Laya self-hosted. Sul fronte qualità arrivano i fix allo structured output per Bedrock e il supporto alle provider options. Aggiornati anche i modelli di default, con Claude 4.7+ che ignora i sampling params.
Laravel 13.35.0: scheduler più controllabile, code più flessibili e response Markdown
Laravel 13.35.0 porta Schedule::alwaysOnOneServer(), il supporto a schedule:interrupt per i comandi di lunga durata, --memory in percentuale per queue:work e il nuovo metodo markdown() sulla response factory. Tante piccole migliorie anche a test, view compilate in parallelo e dischi local.
BashBox: un interprete Bash in sandbox per agenti AI in PHP
BashBox è un interprete bash scritto in puro PHP che permette di eseguire gli script generati dagli agenti AI senza mai ricorrere a proc_open, exec o shell_exec. Ogni comando è una classe PHP che lavora su un filesystem virtuale, con limiti di esecuzione, quote disco e accesso di rete configurabili. Ideale per chi sviluppa agenti con il Laravel AI SDK o altre librerie simili.