optimisation core web vitals 7 min

MySantéMobile : LCP de 4,2s à 1,8s sans toucher aux images

Quand un site de télémédecine mobile-first traîne un LCP à 4 secondes, les images ne sont pas toujours les coupables. Retour sur un chantier Core Web Vitals qui a inversé la tendance.

Par Julien Morel
Partager

On a vu un site de télémédecine perdre 30 % de ses consultations en ligne parce que son LCP dépassait les 4 secondes sur mobile. Le diagnostic initial pointait les images. On les a compressées, converties en WebP, redimensionnées. Résultat : LCP toujours à 4,1 s. Le vrai coupable se cachait dans trois endroits où personne n’avait regardé.

MySantéMobile, c’est une plateforme de rendez-vous médicaux et de téléconsultation, mobile-first, React, déployée derrière un CDN classique. Le genre de site où chaque seconde de latence se traduit par des patients qui décrochent. Le brief tenait en une phrase : « les Core Web Vitals nous tuent, aidez-nous à passer les seuils. » Sauf que les seuils, c’est un indicateur, pas une stratégie.

Lighthouse pointait l’image, l’image n’y était pour rien

Le fichier hero pesait moins de 40 Ko en WebP, servi via CDN. Une fois la requête partie, il se bouclait en 200 ms. Le LCP affichait pourtant 4,2 s. La différence, c’est ce qui se passe avant la requête : cascade de JavaScript, police custom à télécharger, JSON-LD inline de 12 Ko à parser, puis seulement la balise <img loading="lazy"> qui daigne se déclencher. Le LCP mesure le délai jusqu’à l’affichage, pas le poids du fichier.

loading="lazy" sur l’image hero : la fausse bonne pratique

L’argument qu’on entend partout : « le lazy-loading améliore les performances. » Sur les images situées sous la ligne de flottaison, oui. Appliqué à l’élément qui constitue le LCP, il produit exactement l’inverse. L’attribut loading="lazy" indique au navigateur de différer le chargement jusqu’à ce que l’image approche de la fenêtre d’affichage. Problème : l’image hero est déjà dans la fenêtre d’affichage, mais le navigateur doit d’abord terminer le parsing, exécuter le JavaScript et calculer la mise en page avant de déterminer la distance. Résultat, la requête de l’image est repoussée bien après le First Contentful Paint, et le LCP s’effondre.

On a retiré loading="lazy" sur l’image hero et on l’a remplacé par un fetchpriority="high" explicite. En complément, on a ajouté un <link rel="preload" as="image" imagesrcset="..."> dans le <head> pour que le navigateur commence le téléchargement avant même que le parseur HTML n’atteigne la balise <img>. Le LCP est descendu à 2,6 s avec cette seule correction, sans toucher au poids de l’image.

Le seuil documenté pour passer en « bon » reste un LCP sous 2,5 s. On décortique la priorité des ressources et le rôle du préchargement dans notre analyse de l’optimisation des Core Web Vitals.

La police variable Inter bloquait le rendu 800 ms

MySantéMobile utilisait une police variable Inter, chargée via @font-face dans une feuille CSS externe. Comme la feuille de style était liée en blocking dans le <head>, le navigateur suspendait le rendu du texte tant que la police n’était pas téléchargée. Pire, la requête de la police n’était déclenchée qu’après le parsing du CSS, puis le téléchargement prenait environ 800 ms sur une connexion 4G moyenne. Le titre principal, le bouton d’appel à l’action, tout restait invisible pendant ce laps de temps. Le LCP se déclenchait finalement sur le titre une fois la police disponible, ce qui repoussait le temps mesuré au-delà de 4 secondes.

Le correctif a consisté en trois actions. D’abord, un font-display: swap pour afficher immédiatement le texte avec une police système et permuter dès que la police web est chargée ; l’utilisateur voit du contenu lisible sans attendre. Ensuite, un préchargement de la police via <link rel="preload" as="font" crossorigin="anonymous"> pour lancer le téléchargement en parallèle du parsing HTML. Enfin, on a sous-ensemble la police pour ne conserver que les caractères latins utilisés sur les pages principales, réduisant le fichier de 120 Ko à 35 Ko. Le temps de rendu du texte est passé sous la barre des 900 ms sur le terrain, et le LCP a suivi.

⚠️ Attention : le préchargement d’une police mal configuré (absence de crossorigin ou mauvais format) peut provoquer un double téléchargement et empirer la situation. Vérifiez toujours dans l’onglet Network que la police n’est chargée qu’une fois.

Un middleware de cache qui rallongeait le TTFB de 600 ms sans alerter Lighthouse

Le TTFB affiché dans la Search Console tournait autour de 1,1 s, mais Lighthouse en local ne détectait rien d’anormal. L’écart venait d’un middleware de cache placé devant l’application Next.js. Ce middleware vérifiait un cookie de session sur chaque requête, y compris sur les pages publiques comme la landing page. Cette vérification bloquait la réponse jusqu’à ce qu’une base de données Redis réponde, ajoutant entre 500 et 700 ms de latence selon la charge.

Le correctif n’a pas consisté à supprimer le middleware mais à le court-circuiter pour les routes publiques. Une règle simple dans la configuration du CDN : si l’URL correspond à une page statique sans composant authentifié, on ignore le cookie et on sert la version en cache. Le TTFB sur la page d’accueil est descendu à 220 ms. Un middleware, un proxy, une règle de routage mal écrite suffisent à le dégrader sans que Lighthouse ne bronche, parce que l’outil teste souvent depuis un environnement proche du serveur.

L’impact du JavaScript tiers : le pixel de suivi qui coûtait 1,2 seconde d’INP

Une fois le LCP sous 2,5 s, un autre signal a viré au rouge dans la Search Console : l’INP, ou Interaction to Next Paint. Sur mobile, les utilisateurs qui tapaient leur code postal dans le champ de recherche subissaient un délai de réponse de plus de 500 ms avant de voir la liste des praticiens. L’onglet Performance des DevTools montrait que le thread principal était occupé par un script de tracking marketing qui exécutait une fonction d’obfuscation sur chaque frappe.

Ce script n’était pas hébergé par MySantéMobile. Il provenait d’un tag manager chargé d’injecter des pixels publicitaires. Le site ne le contrôlait pas directement. L’audit a révélé que pour chaque événement d’input, le script déclenchait trois appels à JSON.stringify, deux encodeURIComponent et un hachage SHA-256 sur la valeur saisie. Résultat : une tâche longue de 380 ms qui tuait la réactivité.

La solution immédiate a été de déplacer ce traitement dans un Web Worker via une stratégie de délégation. À plus long terme, l’équipe a migré le tag manager vers une solution consent-based qui ne déclenche le suivi qu’après acceptation explicite. L’INP est redescendu sous 200 ms sur le terrain.

Un chantier connexe a porté sur la refonte de l’application React elle-même. La gestion d’état utilisait un store legacy qui recalculait l’intégralité de l’arbre au moindre changement de champ de formulaire. En migrant vers une bibliothèque plus légère comme Zustand, le volume de JavaScript exécuté sur une interaction a été divisé par trois. On détaille cette approche dans notre retour sur le state management React avec Zustand, où l’on explique comment un sélecteur bien conçu évite les rendus en cascade.

ISR pour la landing, SPA pour le tunnel

MySantéMobile tournait en rendu 100 % client. Chaque page, landing comprise, exigeait le bundle React complet avant le moindre pixel. La bonne réponse n’était ni « tout en SSR » ni « tout en SPA », mais un rendu hybride par segmentation de route : ISR régénéré toutes les heures sur la landing, SPA conservée sur le tunnel de prise de rendez-vous avec chargement asynchrone du code d’auth et de la logique CRUD. Bilan : 42 Ko de HTML prêt à afficher contre 340 Ko de JavaScript auparavant. Sur un autre chantier, on a comparé Claude Code et Cursor IDE pour automatiser ce type de découpage par route.

Questions fréquentes

Est-ce que les Core Web Vitals sont vraiment déterminants pour le référencement d’un site de santé ?

Ils font partie des signaux de classement documentés, au même titre que la compatibilité mobile. Sur des requêtes concurrentielles comme « consultation médecin en ligne », un site qui ne passe pas les seuils peut perdre plusieurs positions. Mais l’impact le plus direct est sur le taux de rebond : un LCP supérieur à 3 s triple les abandons sur mobile, ce qui dégrade les signaux d’usage et, indirectement, le classement.

Faut-il forcément adopter Next.js ou un framework SSR pour améliorer ses performances ?

Non. Un rendu statique bien configuré avec un CDN et un découpage intelligent du code peut produire des Core Web Vitals excellents, même sur une SPA. Le SSR résout le problème de l’HTML initial, mais il introduit un TTFB plus élevé si le serveur exécute du code à chaque requête. Le choix dépend de la nature du contenu : des pages majoritairement statiques gagnent peu à être rendues dynamiquement à chaque visite.

Comment identifier le vrai responsable d’un LCP médiocre sans se faire piéger par Lighthouse ?

Le lab data de Lighthouse donne une tendance, mais il ne reflète pas les conditions réelles des utilisateurs. Il faut croiser avec le rapport Core Web Vitals de la Search Console, qui agrège les données de champ (field data) et montre la distribution des temps de chargement par type de connexion et d’appareil. Ensuite, l’onglet Performance des DevTools, avec l’enregistrement d’un profil sur un appareil mobile bridé en CPU 4x et réseau 4G, permet d’isoler la chaîne de dépendances qui bloque le LCP.

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.