J’ai récupéré un projet Symfony qui servait une fiche produit en 2,8 secondes. Premier réflexe : accuser Doctrine. J’ai passé une heure à désactiver des lazy-loads inutiles, à optimiser des jointures. Le temps de réponse n’a pas bougé. Le vrai coupable : le composant HttpFoundation configuré avec un cache HTTP laissé à zéro, comme en local. Le TTFB plafonnait à 2,2 secondes. Le navigateur attendait que le serveur reconstruise chaque page à chaque requête, cache Edge ou pas. Cette anecdote illustre un travers qu’on croise sur presque tous les projets Symfony dont le déploiement a été traité comme une formalité administrative.
Ce ticket ne concerne pas la vélocité brute du framework, souvent irréprochable une fois compilé. Il concerne l’infrastructure d’exécution et les décisions qu’on prend, ou qu’on omet, au moment de pousser en production. On peut écrire les contrôleurs les plus fins, si le Cache-Control renvoie no-cache partout et que les assets sont recompilés sans empreinte, le LCP va pâtir et Googlebot va ramper une bouillie lente.
Le cache HTTP de Symfony n’est pas un détail cosmétique
Le composant HttpFoundation embarque un mécanisme de cache HTTP qui repose sur les en-têtes Cache-Control, ETag et Last-Modified. Trop de projets le désactivent parce que « ça empêche de voir les modifications en recette ». Résultat : la même page est régénérée à chaque visite, sollicitant PHP, la base de données et le moteur de templates. Le Time to First Byte s’envole. Sur un plan produit e-commerce, c’est une porte ouverte à un LCP dégradé, puisque le moindre octet de HTML prend plus de 500 ms avant même le début de la peinture.
Active un cache de page en HTTP avec le kernel event ResponseEvent. Définis une durée de vie publique via $response->setPublic() et $response->setMaxAge(3600). Ajoute un ETag basé sur le contenu ou un hash métier. Tu peux coupler ça à un reverse proxy comme Varnish ou au CDN de ton hébergeur. Ce qui importe, c’est que la couche HTTP serve une réponse 304 ou un hit sans jamais réveiller le noyau Symfony pour les visiteurs non connectés. En pratique, sur le projet qu’on a repris, le simple fait d’activer un Cache-Control: public, s-maxage=600 sur les pages produits a ramené le TTFB à moins de 200 ms pour 90 % des requêtes. Le LCP est passé de 4,1 s à 2,3 s sans toucher une ligne de template.
Un cache HTTP bien réglé agit comme un multiplicateur : il réduit la charge serveur, laisse plus de CPU disponible pour les requêtes non cachables et améliore mécaniquement tous les indicateurs des Core Web Vitals qui dépendent du serveur. Négliger cette brique, c’est allonger la distance entre l’utilisateur et le premier pixel utile, quel que soit le budget alloué aux optimisations front-end.
Assets compilés, hashed, et lazy-loadés : le piège des bundles tout-en-un
Le front-end d’un projet Symfony moderne utilise soit Webpack Encore, soit le plus récent AssetMapper. Dans les deux cas, la règle est la même : chaque fichier CSS ou JS livré en production doit porter une empreinte dans son nom (app.a1b2c3.js). Sans cette technique de cache busting, un visiteur qui a mis en cache l’ancienne version peut recevoir un HTML frais pointant vers des assets périmés. Le navigateur télécharge du CSS incomplet, le style casse, le LCP chute parce que la ressource critique met trois fois plus de temps à se résoudre.
Le piège classique, c’est d’installer un bundle qui promet d’embarquer Bootstrap, jQuery et trois polices en une ligne. La simplicité de configuration masque un chargement synchrone de centaines de kilooctets non utilisés sur la page d’accueil. On obtient un INP à 400 ms parce que le thread principal est obstrué par un fichier JS qui n’a rien à faire là. Préfère un chargement asynchrone des scripts non critiques. Avec AssetMapper, utilise l’attribut defer ou async. Avec Encore, appelle splitEntryChunks() pour isoler le code métier des librairies tierces.
Si tu intègres un front-end plus conséquent, par exemple une SPA React qui communique avec une API Symfony, la légèreté de la gestion d’état devient vite un facteur de performance. Un store global comme Zustand réduit les réconciliations inutiles comparé à un Redux Toolkit surdimensionné pour le besoin. L’empreinte JavaScript reste plus compacte, le bundle se parse plus vite, et l’interaction avec les composants Symfony côté serveur ne subit pas le contrecoup d’un framework front-end alourdi.
PHP-FPM, Swoole, RoadRunner : choisir son runtime sans dogmatisme
La manière dont PHP est exécuté en production détermine le plancher du TTFB. FPM reste le choix par défaut, soutenu par des années de stabilité. Mais un pool FPM mal dimensionné peut laisser des workers inactifs pendant qu’une pointe de trafic épuise ceux disponibles. Le temps de latence grimpe mécaniquement. Passe en mode dynamique avec un pm.max_children calibré sur la mémoire disponible du serveur. Active surtout opcache.preload en pointant vers le fichier config/preload.php de Symfony. Sans préchargement, chaque worker doit recompiler le conteneur d’injection de dépendances au premier appel, ce qui peut ajouter 100 à 150 ms au premier hit post-déploiement.
Les runtimes persistants comme Swoole ou RoadRunner changent la donne : le noyau Symfony est chargé une fois en mémoire et réutilisé sur des centaines de milliers de requêtes sans jamais relire le code source. Sur un serveur modeste, on observe un TTFB divisé par quatre par rapport à FPM sans opcache. La contrepartie, c’est la compatibilité avec les bundles tiers. Certaines librairies historiques stockent un état global dans une propriété statique et fuient entre les requêtes. Testez systématiquement avec un jeu de scénarios métier avant de migrer une application existante. Sur un projet neuf, RoadRunner ou FrankenPHP avec le mode worker de Symfony 6.2+ offre une latence proche du zéro pour les pages non cachées.
Intégrer les Core Web Vitals dans la CI/CD : Lighthouse CI avant la mise en prod
Un pipeline de déploiement qui exécute les migrations, les tests unitaires, mais ignore les mesures de performance, laisse passer des régressions silencieuses. Une mise à jour de chart de design, un nouveau bundle analytics, une refonte du footer. Trois lignes anodines qui font glisser le LCP de 2,0 à 3,4 secondes. Trois semaines plus tard, la Search Console remonte une baisse de la métrique. Le mal est fait, le crawl a déjà enregistré la dégradation.
Lighthouse CI s’intègre à GitHub Actions, GitLab CI ou Bitbucket Pipelines. On lance un audit sur un staging accessible uniquement au runner, avec une assertion sur le LCP, le TTFB et le CLS. Si le score passe sous un seuil défini, le pipeline échoue. Ce n’est pas du perfectionnisme ; c’est une barrière d’entrée qualité qui alerte avant que Googlebot ne juge. La config se résume à un fichier lighthouserc.js et quelques lignes dans le workflow. Pour ceux qui préfèrent générer une base propre sans mémoriser toute la syntaxe, un assistant comme Claude Code peut produire un squelette plus vite qu’un IDE classique. Notre comparatif entre Claude Code et Cursor montre des écarts réels sur la génération de configurations YAML complexes. L’important, c’est d’avoir un seuil et de le contrôler à chaque pull request.
Robots.txt, sitemap et canonical sans casser le crawl
Le pire bug que j’aie causé en déploiement Symfony reste le fichier robots.txt copié du staging en production. Un Disallow: / bien en place, invisible en local parce qu’on naviguait en admin. Le trafic organique a fondu de 60 % en quatre jours. Le framework n’y pouvait rien. Le pipeline de déploiement, lui, ne vérifiait pas l’environnement. Depuis, je milite pour trois contrôles en fin de déploiement qui n’ont aucun coût d’exécution : un curl -I sur la page d’accueil pour vérifier le statut 200 et l’absence de x-robots-tag: noindex ; un diff du robots.txt entre le staging et un fichier de référence versionné ; une validation que le sitemap.xml est bien généré par un bundle comme DekaleeSitemapBundle et qu’il ne renvoie pas un 404 silencieux.
Le SEO d’un projet Symfony ne tient pas qu’aux meta descriptions. Une mauvaise gestion des URLs canoniques, des paramètres de facettes non déclarés dans Google Search Console ou un noindex sur un environnement de recette qui fuit, et vous perdez le contrôle de l’indexation. Activez le disallow par défaut sur les environnements hors production avec un simple APP_ENV=dev dans le virtualhost, et bloquez l’accès aux crawlers par authentification HTTP. Ces mesures sont rarement spectaculaires, mais leur absence est une des premières causes de chute brutale du crawl budget.
Méfiez-vous des bundles magiques qui promettent +50% de performance
Un bundle ne peut pas compenser une absence de cache HTTP ni réparer un opcache désactivé. Les solutions packagées qui annoncent booster les performances activent souvent des couches déjà présentes dans le framework, avec un wrapping qui alourdit l’autoloading. Vous gagnez du temps de configuration, vous perdez en visibilité sur ce qui tourne vraiment. Avant d’installer quoi que ce soit, auditez le Cache-Control, vérifiez opcache_get_status() et profilez avec Blackfire. Le vrai gain se cache dans vos propres mesures.
Questions fréquentes
Faut-il absolument utiliser Docker pour déployer Symfony en 2026 ?
Docker facilite la reproductibilité, mais n’améliore pas la performance intrinsèque. Si votre équipe maîtrise déjà les conteneurs, un environnement uniforme du poste local à la production réduit les bugs silencieux. L’important est que l’image de production embarque l’opcache et utilise un runtime PHP adapté, avec ou sans conteneur.
Comment gérer le cache HTTP quand l’utilisateur est connecté ?
Utilisez le paramètre public ou private en fonction du contexte. Pour des pages panier, le Cache-Control: private, max-age=0, no-cache évite de servir une version en cache à un autre utilisateur. Vous pouvez aussi combiner un cache public avec un ESI (Edge Side Includes) pour les portions dynamiques, si votre reverse proxy le supporte.
Peut-on mesurer le TTFB depuis Symfony directement ?
Oui, injectez Stopwatch dans un event listener kernel.response pour journaliser le temps de génération côté serveur. Cela ne remplace pas une mesure RUM (Real User Monitoring) qui capture le TTFB réseau complet, mais ça aide à identifier une dérive du moteur PHP lui-même.