optimisation core web vitals 8 min

Symfony 2 : pourquoi sa config serveur reste un levier LCP

Avant de blâmer le JavaScript, regardez les headers de cache de votre backend. L’héritage de Symfony 2 rappelle comment un TTFB mal configuré plombe les Core Web Vitals.

Par Julien Morel
Partager

On a mesuré un LCP à 4,4 secondes sur une landing B2B qui embarquait moins de 90 Ko de JavaScript. Le TTFB, lui, naviguait autour de 2,3 secondes. En désactivant le cache applicatif, chaque requête reconstruisait le corps de page via un moteur de templates PHP, interrogeait la base et recalculait des blocs qui n’avaient pas changé depuis juin 2024. L’équipe front avait passé trois semaines à réduire les polices et à implémenter le fetchpriority="high" sur l’image hero : le gain net fut de 180 ms. Le gisement de performance se trouvait côté serveur, dans une logique de cache HTTP qu’un framework sorti en 2011, Symfony 2, savait déjà formaliser avec une précision chirurgicale.

Je ne vais pas te faire un tutoriel d’installation de Symfony 2 en 2026, ce serait absurde. Le framework vit dans des maintenances long terme de projets d’assurance et de quelques intranets critiques. Ce qui reste pertinent, c’est le modèle de configuration du cache qu’il a imposé à une génération de développeurs PHP : un noyau HTTP qui accepte de raisonner en termes de réponse publique, de durée de vie partagée et de revalidation. Si tu bosses sur du Next.js App Router, du Remix ou du Laravel en 2026, les mécanismes sous-jacents sont les mêmes. C’est le même Cache-Control, le même ETag, la même chaîne de décision qui sépare une page servie en 80 ms d’un TTFB qui étrangle ton LCP.

Ce que le cache HTTP valide avant même ton premier octet

Un navigateur ou Googlebot qui demande une page déclenche une cascade que les audits Lighthouse appellent « temps jusqu’au premier octet ». Derrière cet indicateur, il y a la pile d’exécution du backend : résolution de route, lecture de session, requêtes SQL, assemblage du template. Symfony 2 imposait un kernel.request et un kernel.response qui permettaient d’injecter des couches de cache avant même que la logique métier ne s’exécute. L’idée centrale : si le corps de réponse est déjà stocké dans un cache inverse (Varnish, Fastly, le Edge Cache de ton hébergeur), le framework n’est jamais sollicité.

En pratique, cela se traduisait par des entrées de configuration comme celle-ci :

# app/config/cache.yml
cache_control:
    public: true
    max_age: 600
    s_maxage: 3600
    must_revalidate: false

Cette brique, héritée du composant HttpCache de Symfony, active deux choses. D’abord, s_maxage autorise un CDN ou un reverse proxy à conserver la page dans son propre cache partagé sans repasser par le serveur d’origine. Ensuite, must_revalidate: false évite une requête conditionnelle chaque fois qu’un utilisateur revient en arrière. Sur un site à fort volume de crawl, c’est ce qui fait basculer le TTFB sous la barre des 100 ms, sans toucher une seule ligne du front.

Un cas classique sur les déploiements Symfony 2 mal configurés : un développeur ajoute Vary: Cookie dans les headers de réponse pour gérer les sessions utilisateur, pensant protéger des pages personnalisées. Résultat immédiat : chaque requête qui transporte un cookie de session (même un cookie analytics) génère un cache-miss. Le reverse proxy traite Cookie comme une variation différente, et la page n’est presque jamais servie depuis le cache. Le TTFB monte en flèche.

La correction consiste à ne pas varier sur Cookie pour les pages publiques et à externaliser la personnalisation. Symfony 2 utilisait pour cela les ESI (Edge Side Includes), une technique qui consiste à marquer un fragment de page comme dynamique pendant que le reste du document provient du cache. Le serveur assemble le tout en bordure de réseau. L’effet sur le LCP est mécanique : le squelette HTML arrive vite, le navigateur parse le DOM et commence à charger les ressources critiques sans attendre le fragment lent.

# Activation des ESI dans le kernel Symfony 2
framework:
    esi: { enabled: true, renderer: hinclude }

Si vous utilisez aujourd’hui un Next.js avec stale-while-revalidate sur Vercel ou un setResponseHeaders dans vos edge functions, vous manipulez le même concept. L’article optimisation core web vitals explore les impacts mesurés sur le LCP quand cette logique cache est absente.

⚠️ Attention : vérifier le Vary ne sert à rien si votre CDN ignore l’en-tête. Certains fournisseurs désactivent la variation sur Cookie par défaut. Testez avec un curl -svo /dev/null -H "Cookie: session=foo" pour être certain du comportement.

Pourquoi les audits front seuls ne voient pas ça

Un audit de state management react zustand peut identifier un re-render intempestif qui retarde le First Contentful Paint. Mais aucun outil front (Lighthouse, WebPageTest en mode simple, PageSpeed Insights) ne descend au niveau de la configuration du cache applicatif. Ces outils peuvent signaler un TTFB élevé, sans le décomposer en « temps réseau + temps processing backend + temps cache lookup ». C’est à vous de remonter la chaîne.

La configuration historique de Symfony 2 rendait ce diagnostic plus transparent : le profiler intégré capturait chaque couche de l’événement HTTP et affichait le temps passé dans le kernel, les listeners, le cache. Sur une app moderne, il faut reconstituer ce même niveau de détail avec des outils de tracing OTel ou des logs serveur. Mais le point d’entrée reste le même : si le TTFB dépasse 800 ms et que la page est publique, la première hypothèse est un défaut de mise en cache HTTP, pas un problème de useEffect ni de lazy().

Appliquer les leçons de Symfony 2 à une stack jamstack

Les architectures jamstack pensent souvent le cache au niveau du CDN, mais oublient de le déclencher explicitement. J’ai vu des sites Gatsby ou Astro dont chaque page HTML était régénérée à chaque requête parce que le max-age était absent du vercel.json ou du _headers. On se retrouve avec un TTFB de 600 ms sur une page statique, ce qui est du même ordre que le pire PHP non caché d’il y a quinze ans.

Les deux leviers hérités de Symfony 2 qui restent actionnables :

  • Définir un Cache-Control agressif pour les pages publiques. public, max-age=3600, s-maxage=86400 pour une fiche produit qui ne change pas en moins d’une journée.
  • Utiliser des fragments dynamiques conditionnels. Plutôt qu’invalider toute la page, ne rafraîchissez que le bloc « disponibilité » ou « prix » via une edge function qui pointe un endpoint de stock, comme le faisaient les ESI.

Un développeur front qui ne lit jamais les headers HTTP de sa page de production opère avec une dette qu’il ne mesure pas. Cette dette, on la voit apparaître dans la Search Console derrière des URL détectées mais indexées avec retard, parce que Googlebot ajuste sa fréquence de crawl selon la fraîcheur des réponses.

Micro-section : la route qui ne doit jamais atterrir dans le cache

Une erreur encore fréquente : cacher une page de panier ou un dashboard administrateur avec les mêmes directives que les pages publiques. Symfony 2 isolait ces routes via un response->setPrivate() dans le contrôleur. Si un cache partagé stocke un fragment contenant les identifiants d’une session fuyante, le désastre n’est pas SEO, il est GDPR. L’impact indirect sur l’indexation est pourtant réel : Google testant des URL protégées peut découvrir des états de page qu’il ne devrait jamais voir et gaspiller du crawl budget.

Questions fréquentes

Pourquoi ne pas simplement utiliser un CDN sans toucher au backend ?

Le CDN obéit aux en-têtes que votre backend lui envoie. Sans Cache-Control configuré, le CDN se contente de relayer la requête, et vous ne gagnez rien. La configuration du cache commence toujours côté application.

Faut-il encore utiliser des ESI avec les frameworks modernes ?

La technique des ESI est largement remplacée par les edge functions et l’incremental static regeneration. Le principe de découper une page en zones à durée de vie différente reste valide ; seul l’outillage change. Si votre hébergeur ne supporte pas les ESI, utilisez du stale-while-revalidate par fragment.

Quel lien entre la config Symfony 2 et le choix d’un IDE comme Cursor ou Claude Code ?

Aucun lien direct, mais le choix de l’environnement de développement influe sur la capacité à débugger rapidement une boucle de cache. Vous trouverez des éléments de comparaison dans notre retour sur claude code vs cursor ide.

Articles similaires

Julien Morel

Julien Morel

Ancien dev front React passé SEO technique après une migration e-commerce qui a fait perdre 60% du trafic organique à son employeur en une nuit (fichier robots.txt oublié en staging). Depuis, il écrit pour que ça n'arrive à personne d'autre et teste sur ses propres side-projects avant de publier quoi que ce soit.

Cet article est publie a titre informatif. Faites vos propres recherches avant toute decision.