optimisation core web vitals 9 min

Load balancing et TTFB : l’angle mort des Core Web Vitals

Vos temps de réponse varient sans raison apparente ? Un load balancer mal réglé peut faire fluctuer le TTFB, casser le cache et dégrader le crawl. Voici comment le diagnostiquer.

Par Julien Morel
Partager

Un matin de novembre, un client nous envoie un rapport CrUX avec une métrique LCP en dents de scie. La page d’accueil passait de 2,1 s à 4,7 s d’un jour à l’autre, sans déploiement. Dans les logs, rien. Le serveur d’origine répondait en 90 ms. On a fini par isoler le coupable avec un script curl en boucle : le load balancer envoyait une requête sur cinq vers un nœud dont le cache HTTP n’était pas encore chaud, et ce nœud renvoyait un TTFB de 700 ms au lieu de 120. Le LCP suivait.

Quand on parle performance web, on pense bundles JS, lazy-loading, fetch priority. Le load balancing reste la couche invisible que personne n’audite, parce qu’elle appartient au DevOps, pas au SEO. Pourtant, c’est un levier direct sur le TTFB, donc sur le LCP, et un perturbateur sournois du crawl. Multiplier les serveurs ne réduit pas mécaniquement la latence. Sans une stratégie de cache cohérente et une mesure par nœud, vous ajoutez des points de défaillance discrets.

Pourquoi le TTFB fluctue avec plusieurs serveurs

Le Time to First Byte se joue en grande partie avant que votre application ne produise le moindre HTML. Chaque nœud derrière le load balancer a son propre état : cache applicatif, connexions base de données, compilation JIT dans le runtime. Quand la répartition de charge distribue les requêtes sans tenir compte de cet état, le TTFB change d’un nœud à l’autre. Ce n’est pas une question de puissance CPU. C’est une question de chaleur du cache.

Prenons un site Next.js avec un cache incrémental. Le premier visiteur qui tombe sur un nœud froid subit un rendu complet : fetch API, exécution serveur, génération du HTML. Les suivants reçoivent une page en moins de 100 ms. Si le load balancer utilise du round-robin simple, 20 à 33 % des requêtes atterrissent sur un nœud froid après un redémarrage ou une purge de cache. Googlebot, lui, arrive souvent en premier après une phase d’inactivité. Il enregistre le TTFB le plus élevé.

Les seuils Core Web Vitals ne tolèrent pas cette variabilité. Un LCP « bon » exige un TTFB inférieur à 800 ms dans 75 % des cas. Dès que 25 % des requêtes partent sur un nœud froid avec un TTFB à 700 ms, vous êtes en limite. Si le nœud froid dépasse 800 ms, vous basculez dans le orange, sans jamais avoir touché une ligne de code.

Le piège du cache local par serveur

La logique classique consiste à dire : on ajoute un serveur, on double la capacité. Mais si chaque serveur maintient son propre cache d’objets (page HTML, fragments, réponses API), chaque nouveau nœud repart de zéro. Le load balancer ne sait pas qu’une page a déjà été générée par un autre nœud. Il envoie la requête là où la file d’attente est la plus courte, pas là où le résultat existe déjà.

Résultat : le taux de cache hit chute. Un site avec deux serveurs derrière un round-robin peut voir son hit ratio descendre à 50 % au lieu des 90 % visés, simplement parce que la même URL est demandée alternativement sur deux nœuds distincts. Le mécanisme est simple : la première requête remplit le cache du serveur A ; la deuxième, distribuée sur le serveur B, le remplit une seconde fois. À chaque purge ou expiration, le problème recommence. On paie deux fois le coût de génération pour la même ressource.

Les conséquences sur l’indexation sont mesurables. En analysant les logs d’un site e-commerce de 50 000 URLs, on a constaté que Googlebot mettait en moyenne 2,5 tentatives avant d’obtenir une réponse avec un TTFB inférieur à 500 ms. Pendant ce temps, les signaux Web Vitals remontaient des données dégradées, et la couverture d’indexation plafonnait.

⚠️ Attention : un cache local par serveur, couplé à une purge non atomique, peut servir des versions différentes d’une même page à Googlebot selon le nœud qu’il atteint. L’algorithme peut alors choisir une version obsolète comme canonique, ou pire, détecter une incohérence.

Le round-robin n’est pas une stratégie de cache

Beaucoup d’architectures naissent avec un NGINX en reverse proxy configuré en proxy_pass avec upstream basique. C’est du round-robin. Cette méthode est idéale pour des services stateless ou des APIs qui ne dépendent pas d’un cache applicatif. Mais dès que le serveur d’application construit du HTML avec un coût CPU significatif, le round-robin casse l’efficacité du cache.

Le vrai problème, c’est le TTFB perçu par le visiteur final et par Googlebot. Quand le navigateur ou le bot tape l’URL, il n’a aucun moyen de savoir quel nœud répondra. Une requête sur deux peut être rapide, l’autre lente. Sur mobile avec une latence réseau déjà pénalisante, les 600 ms de différence entre nœud chaud et nœud froid s’ajoutent à la latence de transmission, au temps SSL, au DNS. Vous passez d’un LCP à 2,5 s à 4 s sans changer de page.

On a mesuré le phénomène avec un banc d’essai Express.js et deux instances derrière HAProxy en round-robin. Le premier hit prenait 320 ms, le second sur l’autre instance 340 ms. Une purge de cache ciblée faisait remonter l’un des deux à 750 ms. Avec un curl en boucle, le TTFB moyen restait bas, mais le p75 grimpait. C’est exactement ce que mesure le champ LCP du rapport CrUX.

Ce que Googlebot voit vraiment

Googlebot n’est pas un visiteur régulier. Ses passages dépendent de la popularité des URLs, du crawl budget, de la fraîcheur du sitemap. Il arrive souvent en premier sur une page fraîchement mise en cache ou, à l’inverse, sur un nœud qui vient de redémarrer. Il ne bénéficie pas du préchauffage des utilisateurs.

Quand le load balancer utilise une persistance de session (sticky session), Googlebot peut être assigné à un nœud via un cookie. Si ce cookie bloque le cache CDN ou modifie le contenu servi, vous créez une version différente de la page pour le bot. Pire, un cookie de session envoyé par le load balancer peut être traité comme un paramètre unique, multipliant les URLs crawlées et diluant le crawl budget. On a déjà vu un site perdre 30 % d’URLs indexées à cause d’un cookie de stickiness non filtré dans la Search Console.

Pour un site à fort volume, la solution consiste à externaliser le cache avant le load balancer. Un CDN correctement configuré absorbe les hits et sert le HTML complet sans jamais interroger l’origine, sauf expiration. Dans ce schéma, le load balancer ne voit que les requêtes de purge et de revalidation, bien moins nombreuses. Le TTFB devient celui du CDN, homogène sur tous les pops.

Un test simple pour auditer votre load balancer

Si vous n’avez pas accès à la configuration réseau, un script bash minimal vous donne déjà une idée de la variabilité. Répétez une requête sur la même URL 50 fois en boucle, en désactivant le cache local :

for i in $(seq 1 50); do
  curl -s -o /dev/null -w "%{time_starttransfer}\n" -H "Cache-Control: no-cache" https://example.com/page/
done

Récupérez la distribution des temps. Un écart supérieur à 200 ms entre le minimum et le 75e centile signale une irrégularité liée à l’instance cible. Ajoutez l’en-tête X-Server-ID si vos backends l’exposent, pour corréler la lenteur à un nœud spécifique. Le diagnostic ne demande pas les droits root.

Vous pouvez aussi comparer les temps de réponse avec un outil comme WebPageTest en multi-run (9 tests) et observer la variance du TTFB. Si les premières vues sont lentes et les vues répétées rapides, le cache d’origine est en cause. Si même les vues répétées varient, le load balancer distribue mal.

💡 Conseil : testez vos URLs avec un user-agent Googlebot via curl -A "Mozilla/5.0 (compatible; Googlebot/2.1; +https://www.google.com/bot.html)" pour vérifier que le load balancer ne traite pas le bot comme un utilisateur connecté. Un Vary: User-Agent mal configuré peut également fragmenter le cache.

Sticky session, cohérence du cache et alternatives

La persistance de session résout partiellement le cache froid : un visiteur reste sur le même nœud. Mais cette approche apporte d’autres problèmes. Le cookie de stickiness allonge chaque requête, même pour les assets statiques, sauf règle explicite. Certains CDN refusent de cacher une réponse avec Set-Cookie. Enfin, si un nœud tombe, toute la session est perdue pour l’utilisateur.

L’alternative efficace repose sur un cache partagé en amont ou en latéral. Un reverse proxy unique devant les backends (Varnish, NGINX avec proxy_cache, CDN) garde une copie unique de chaque réponse, peu importe le nœud qui l’a générée. Les serveurs d’application deviennent alors des ouvriers interchangeables. Le TTFB perçu par Googlebot et les utilisateurs ne dépend plus du nœud cible mais de la couche cache.

Sur un projet récent déployé derrière Cloudflare avec un cache HTML basé sur les headers Cache-Control et un purge précise, le p75 du TTFB est passé de 520 ms à 95 ms en une semaine. Aucune modification du code métier, juste un changement de stratégie de répartition. Le rendu côté serveur restait aussi lourd, mais il était exécuté une fois toutes les 30 minutes au lieu de l’être pour chaque visiteur.

Cette architecture a aussi réduit de 40 % la charge CPU sur les instances, libérant du budget pour des tâches asynchrones. Les développeurs front ont pu se concentrer sur l’optimisation du bundle JS sans que le TTFB vienne plomber leurs efforts. D’ailleurs, quand on optimise le state management de l’application, une librairie comme Zustand permet de réduire les cycles de rendu côté client après l’hydratation, ce qui profite à l’INP ; mais sans TTFB stable, l’utilisateur ne voit jamais la page.

Questions fréquentes

Le load balancing est-il pertinent pour un site statique hébergé sur un CDN ? La plupart du temps, oui, mais le problème se déplace. Le CDN joue le rôle de répartiteur global. La variance vient alors des pops géographiques ou de la purge du cache CDN. L’audit doit porter sur le TTFB par région, pas par serveur d’origine.

Comment distinguer un problème de load balancer d’un problème de base de données ? Ajoutez un header de debug indiquant le nœud et comparez les temps de réponse pour une route simple sans accès base (une page statique ou un endpoint /health). Si la variance persiste sur une route sans base, le load balancer est en cause, ou le runtime serveur.

Cloudflare en mode proxy règle-t-il le problème de cache par nœud ? Partiellement. Cloudflare met en cache le HTML selon vos règles, ce qui masque la variance des backends. Mais si le cache expire souvent ou si vous utilisez le Bypass Cache sur certaines URLs, le problème réapparaît. La clé est d’avoir un TTFB d’origine bas pour les requêtes non cachées.

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.