On a vu un LCP à 5,1 secondes sur mobile pour un site e-commerce qui se croyait optimisé. La cause ? Un bundle allégé servi spécifiquement aux user-agents mobiles qui se révélait cassé sur les dernières versions de Chrome Android. Le polyfill qui aurait dû remplacer une API absente n’était jamais chargé parce que le serveur avait décidé que « mobile = vieux navigateur ». Résultat : un First Contentful Paint correct, mais un LCP repoussé par des erreurs JavaScript silencieuses. La détection de profil côté serveur, quand elle s’appuie sur l’user-agent, peut anéantir des semaines d’optimisation.
Ce n’est pas un cas isolé. On vous dira que servir une page allégée en fonction de l’appareil est une technique qui a fait ses preuves. La réalité technique de 2026 est plus brutale : les stratégies basées sur l’user-agent introduisent une fragmentation de cache, des incohérences de rendu entre Googlebot et l’utilisateur réel et une dette de maintenance qui se paie directement sur le LCP. L’alternative, c’est la feature detection côté client, couplée à des choix d’architecture qui renforcent les signaux des Core Web Vitals au lieu de les parasiter.
La promesse trahie de l’optimisation par user-agent
L’idée est séduisante : intercepter l’en-tête User-Agent sur le serveur, en déduire le type d’appareil ou de navigateur, et servir une version spécifique du HTML, des CSS et du JavaScript. Moins de code expédié, donc moins de parsing et d’exécution, donc un meilleur LCP. Le problème, c’est que cette promesse repose sur deux hypothèses fausses : la stabilité des user-agents et la fiabilité de leur interprétation.
Les user-agents sont une soupe syntaxique instable. Un même appareil peut envoyer des chaînes différentes selon le WebView utilisé, le mode économie d’énergie ou la présence d’un VPN. La base de correspondances côté serveur devient alors une usine à faux positifs. Vous croyez servir une version allégée à un iPhone 14, mais vous touchez un Chrome desktop en mode responsive design. Ou pire : vous servez l’expérience desktop à un Samsung pliant qui déclare un user-agent Chrome classique malgré son écran étroit.
La maintenance de ces mappings est une charge continue. Chaque mise à jour majeure de navigateur introduit de nouvelles sous-chaînes et en déprécie d’autres. Sur un projet à 200 000 URLs, on a mesuré qu’un dixième des user-agents ne correspondaient à aucune règle connue après seulement six mois sans mise à jour du dictionnaire. Ce n’est pas un bug, c’est le fonctionnement normal d’un écosystème fragmenté.
⚠️ Attention : Les bibliothèques de détection serveur comme ua-parser-js ou mobile-detect ont un coût CPU non négligeable. Sur un serveur Node.js, chaque requête qui parse une chaîne user-agent ajoute 0,3 à 1 ms de temps de traitement, ce qui se répercute directement sur le TTFB quand le trafic monte.
Quand Googlebot voit ce que l’utilisateur ne voit pas
L’impact le plus sournois de la détection serveur concerne le crawl. Googlebot envoie un user-agent spécifique. Si votre serveur décide de lui servir une version allégée ou au contraire une version sans certaines ressources conditionnelles, vous créez un delta entre ce que Googlebot indexe et ce que l’utilisateur perçoit.
On a diagnostiqué un cas où un site servait à Googlebot un rendu complet, exécuté côté serveur, sans aucun JavaScript. L’utilisateur mobile, lui, recevait un squelette HTML et un bundle de 600 Ko. Le LCP mesuré dans la Search Console était excellent, mais le LCP réel sur le terrain (CRuX) était à plus de 6 secondes. Ce décalage a duré huit mois, le temps que le propriétaire du site comprenne pourquoi ses taux de conversion mobile s’effondraient alors que les signaux de classement semblaient sains.
Le risque de cloaking involontaire n’est jamais loin. Une règle serveur qui filtre par user-agent et supprime un bloc de contenu pour les mobiles peut s’appliquer au crawler Googlebot smartphone. Si ce contenu faisait partie du corpus indexable, sa disparition brutale dans la version crawlisée peut entraîner un déclassement sémantique. La Search Console ne vous enverra pas un mail « Cloaking détecté », mais vous verrez les impressions fondre sur les requêtes ciblant ce bloc.
La feature detection évite de deviner
La feature detection inverse la logique. Au lieu de demander au serveur « qui es-tu ? », le navigateur teste localement ce qu’il sait faire. Le principe est simple : une condition dans le code vérifie la présence d’une API ou d’un support CSS avant d’exécuter un traitement ou d’afficher un composant. Le HTML initial est unique, identique pour tous, et c’est le navigateur qui décide, en temps réel, d’embarquer ou non une amélioration.
if ('IntersectionObserver' in window) {
// lazy-load natif, pas de polyfill
} else {
// chargement statique du polyfill
}
Ce qui rend l’approche robuste pour le LCP, c’est qu’elle ne repose sur aucune base externe. La capacité testée est binaire, vérifiée au moment de l’exécution, et insensible aux mutations de l’user-agent. Le serveur renvoie le même HTML, ce qui rend le cache CDN parfaitement unifié. Un seul objet en cache pour 100% du trafic, quel que soit le terminal. Ce point unique réduit la pression sur le TTFB et supprime les erreurs de correspondance.
Certes, la feature detection suppose d’expédier un bundle de base qui contient les tests et les alternatives. Cette charge supplémentaire se mesure en quelques centaines d’octets, pas en dizaines de kilo-octets. Ce qu’on y « perd » en poids brut, on le regagne en stabilité de cache et en élimination des polyfills chargés inutilement. Surtout, on supprime les faux négatifs qui obligeaient un navigateur moderne à télécharger un polyfill de 180 Ko parce que le serveur l’avait classé à tort comme « vieux mobile ».
💡 Conseil : Pour que la feature detection ne retarde pas le LCP, exécutez les tests critiques en inline dans le
<head>et chargez les polyfills avecasyncoudefer. Une détection réalisée dans un script bloquant de 2 Ko en ligne pèse moins lourd qu’un TTFB dégradé par une fragmentation de cache.
Cache, CDN et fragmentation : l’addition serveur que personne ne voit
Chaque variation de réponse basée sur l’user-agent crée une entrée de cache distincte dans le CDN. Si votre règle serveur distingue cinq familles de navigateurs et trois catégories d’appareil, vous multipliez par quinze le nombre d’objets à stocker pour une même URL. Le taux de hit cache s’effondre dès que le trafic se répartit entre ces variantes.
Sur un site à fort volume, un cache dégradé signifie que chaque requête remonte plus souvent jusqu’au serveur d’origine. Le TTFB médian grimpe, parfois de 300 à 400 ms, simplement parce que le CDN passe son temps à interroger l’origine faute d’avoir la bonne variante en mémoire. Cette latence additionnelle se répercute directement sur le LCP, même si le poids de la page a été effectivement réduit.
Quand on couple cette fragmentation à une purge ciblée, le scénario devient ingérable. Une mise à jour de la règle de détection oblige à invalider toutes les variantes pour une même URL, sous peine de servir des pages incohérentes. Certains CDN facturent les purges par objet. Le coût opérationnel et financier peut dépasser le gain théorique de performance, surtout sur un catalogue e-commerce de plusieurs centaines de milliers de fiches.
Charger l’essentiel, améliorer progressivement
L’alternative tient en un principe : servez un seul HTML, structuré pour le rendu initial, et ajoutez les couches d’amélioration en fonction des capacités détectées localement. Le socle HTML doit contenir le contenu visible sans JavaScript, les balises de priorité (fetchpriority="high" sur l’image LCP) et un style critique en ligne. Ensuite, le navigateur active les composants interactifs un par un, uniquement s’ils sont supportés.
Ce modèle n’exclut pas totalement une adaptation au contexte. Vous pouvez utiliser les Client Hints HTTP (Sec-CH-UA-Mobile, Sec-CH-DPR) pour indiquer au serveur des préférences de format d’image ou de densité. Ces en-têtes sont gérés par le navigateur, standardisés, et ne cassent pas le cache s’ils sont utilisés avec le header Vary de manière limitée. Mais la différenciation reste périphérique : elle ne décide pas du contenu ni de l’architecture du bundle, seulement de la résolution d’une image ou du fichier vidéo servi.
La logique est la même que celle qui prévaut dans le choix d’une bibliothèque de gestion d’état côté client : il faut un conteneur mince, réactif, capable d’évoluer sans réécrire toute la couche de rendu. C’est typiquement ce qu’apporte Zustand pour React : un état partagé en mémoire qui reflète les capacités détectées et les met à disposition des composants sans re-rendus en cascade.
Ce qu’on a mis en place sur un Next.js 14 à 200k URLs
Un projet e-commerce avec 200 000 fiches produit servait trois variantes HTML selon l’user-agent. Le LCP mobile terrain dépassait 4 secondes, le cache CDN peinait à 60 % de hit rate, et la moitié des utilisateurs tablette recevaient la version desktop. On a supprimé l’intégralité du routage conditionnel serveur en deux jours.
Le nouveau socle se résume à une route unique, un seul HTML. Le <head> embarque un petit script inline qui teste IntersectionObserver, ResizeObserver et loading="lazy". Selon les résultats, il injecte dynamiquement les polyfills via document.createElement('script'), sans bloquer le parsing. L’image LCP est préchargée via un <link rel="preload"> avec fetchpriority="high". Les variantes d’image sont déléguées à un service d’images qui utilise les Client Hints côté serveur, sans modifier le HTML.
Résultat après déploiement : le hit rate CDN est passé à 98 %, le TTFB médian a baissé de 210 ms, et le LCP mobile mesuré dans le CRuX a gagné 820 ms. La Search Console n’a signalé aucun pic d’erreur. Le maintien de l’indexation a été transparent parce que Googlebot a reçu exactement le même contenu que Chrome mobile, aux images près. Ce n’est pas la feature detection qui a fait ce gain ; c’est la suppression des variations cache et la fin des erreurs de classification qui ont libéré la latence.
La feature detection s’adosse à une connaissance locale et instantanée du navigateur, un peu comme le fait un IDE moderne quand on compare Claude Code et Cursor IDE sur l’autocomplétion : ce n’est pas une configuration statique qui dicte le comportement, c’est le contexte d’exécution réel, scruté à chaque frappe.
Questions fréquentes
Pourquoi ne pas utiliser les Client Hints pour toute la logique adaptative côté serveur ?
Les Client Hints comme Sec-CH-UA-Mobile sont utiles pour sélectionner des formats d’image ou des densités, mais ils ne fournissent pas une information fiable sur le support des API JavaScript ou CSS. Un appareil mobile récent peut parfaitement supporter IntersectionObserver et CSS Grid. Déléguer à ces en-têtes la décision d’inclure ou non un polyfill reproduit le même écueil que l’user-agent, avec des faux positifs en prime.
La feature detection ne pénalise-t-elle pas les navigateurs plus lents qui doivent exécuter les tests ?
Un bloc de détection inline bien écrit pèse moins de 2 Ko minifiés et s’exécute en moins de 10 ms sur un appareil d’entrée de gamme. Le temps passé à tester est inférieur au temps perdu par un cache fragmenté qui oblige à attendre une réponse de l’origine. Les navigateurs lents bénéficient justement de l’absence de polyfill chargé inutilement puisqu’on ne leur sert jamais de code qu’ils ne peuvent pas exécuter.
Peut-on mixer user-agent et feature detection pour du legacy critique ?
Mixer les deux crée une double maintenance et un risque de comportements contradictoires. Si un composant est conditionné par l’user-agent côté serveur mais que la feature detection le réactive côté client, la page peut se construire deux fois, ce qui dégrade l’INP. Mieux vaut choisir une stratégie unique, et si des navigateurs très anciens doivent être supportés, placer la feature detection dans un polyfill global chargé uniquement pour ceux qui échouent à un test de base.