optimisation core web vitals 7 min

Détection de profil côté serveur : le piège du user-agent sniffing

Pourquoi détecter le navigateur côté serveur peut vous coûter 40 % de votre crawl et comment la détection de fonctionnalités (feature detection) améliore vos Core Web Vitals.

Par Julien Morel
Partager

Vendredi 11 mai 2026, 9h15. Un client ouvre sa Search Console devant nous : 12 400 URLs viennent de passer en « Détectée, actuellement non indexée ». Aucune erreur 4xx, aucun noindex, le sitemap est propre. Le coupable, trouvé en six minutes dans les middlewares Next.js, sniffe le header User-Agent pour décider si la page doit servir un layout « desktop » ou un layout « mobile » allégé. Googlebot, qui se présente comme un smartphone sous Chrome, reçoit une version HTML amputée de blocs entiers. Les URLs ont été déclassées sans sommation. Ce cas n’est pas une anomalie : le user-agent sniffing côté serveur reste une cause sournoise de perte de crawl, alors qu’une alternative existe depuis longtemps, la détection de fonctionnalités. Encore faut-il savoir la mettre en œuvre sans casser son rendu hybride ni dégrader ses Core Web Vitals.

Le user-agent sniffing côté serveur : pourquoi Googlebot le lit comme du cloaking

Quand on interroge un middleware ou une fonction serveur avec req.headers['user-agent'], on branche la logique de rendu sur une chaîne de caractères que Google ne contrôle pas. Le bot officiel envoie une UA compatible Chrome mobile. À première vue, lui servir la version mobile paraît cohérent. Le piège est ailleurs : la version qu’il reçoit diffère de celle vue par un crawler plus ancien, par les outils de test, ou par un utilisateur réel dont l’UA ne matche pas exactement la regex maison. Le système de classement interprète ces écarts comme une tentative de cloaking, même involontaire.

Pire, le sniffing serveur empêche un cache HTTP efficace. Un CDN configure son cache sur la variation Vary: User-Agent. Chaque visiteur, chaque bot, chaque UA légèrement différente engendre une entrée de cache distincte. Sur un site e-commerce avec 80 000 fiches produits, le ratio de hit chute brutalement. Le LCP gonfle parce que le serveur d’origine est sollicité pour chaque requête au lieu de servir une réponse en edge. On se retrouve à débugger un LCP à 3,8 secondes en se demandant pourquoi le TTFB oscille entre 90 ms et 900 ms.

Un argument revient souvent : « Google recommande de servir le même contenu à tous les utilisateurs, mais autorise des ajustements mineurs selon l’appareil ». Le mot « mineur » change tout. Si votre logique serveur retire un bloc de prix, une galerie image ou une section de navigation selon l’UA, le delta de contenu est trop grand. La Search Console ne vous enverra pas de message explicite « cloaking détecté ». Elle se contentera de laisser vos URLs croupir dans l’onglet des pages explorées mais non indexées.

Feature detection côté client : un signal fiable sans casser le rendu

La détection de fonctionnalités ne s’intéresse pas à qui appelle, mais à ce que le navigateur sait faire. Plutôt que de parser navigator.userAgent, on teste 'IntersectionObserver' in window ou CSS.supports('display', 'grid'). Cette approche s’exécute côté client, sur le même thread que l’interprétation du DOM. Elle n’introduit aucune variation du HTML servi par le serveur. Googlebot reçoit le même markup que tout le monde, avec les polyfills ou les améliorations progressives qui s’activent uniquement quand le moteur de rendu le supporte.

L’impact sur le crawl est immédiat. L’URL ne génère plus qu’une seule version indexable. Le CDN peut imposer un Cache-Control agressif sans se soucier de Vary. Les métriques de performance y gagnent parce que le premier octet ne dépend plus d’une regex au niveau middleware. Sur le même site e-commerce, on a mesuré un TTFB médian divisé par deux après avoir viré le sniffing UA et placé les ajustements d’affichage dans un script inline de 300 octets.

Reste le scepticisme légitime : la détection côté client ne risque-t-elle pas d’ajouter du JavaScript et de retarder l’affichage ? La réponse tient dans la taille du script. Si on se limite à tester trois ou quatre capacités clés et qu’on injecte le résultat dans une variable globale lue par le framework, l’empreinte est inférieure à 1 ko compressé. Le navigateur parse ce code avant le premier rendu visible, sans bloquer le chargement des ressources critiques. L’INP ne bouge pas d’un pouce. Sur un site interactif, on peut même coupler cette détection avec une solution de state management React comme Zustand pour propager l’information à toute l’application sans re-rendus en cascade.

Quand l’edge function remplace le middleware UA par des en-têtes HTTP utiles

Il existe un entre-deux qui évite le sniffing d’UA tout en conservant une logique serveur : les edge functions. Déployées au plus près de l’utilisateur, elles lisent des en-têtes neutres comme Save-Data, Viewport-Width ou ECT (Effective Connection Type). Ces en-têtes décrivent les capacités réseau et l’affichage sans identifier un navigateur précis. Votre fonction peut décider de servir des images en basse résolution, de retarder un script non essentiel ou d’activer un rendu squelette plus léger. Googlebot n’envoie pas ces en-têtes, il reçoit la version par défaut, complète. Aucun risque de cloaking.

L’adoption est simple avec les plateformes modernes. Une edge function de 20 lignes vérifie la présence de Save-Data: on et ajuste le poids des assets. Pas de Vary, pas de cache fragmenté. Le TTFB reste sous les 50 ms parce que tout est résolu en edge. En local, on simule ces en-têtes avec un client HTTP comme curl ou l’extension de navigateur idoine. Si vous cherchez un environnement de développement capable d’intercepter et de rejouer ces headers sans quitter l’IDE, notre retour sur Claude Code vs Cursor IDE détaille les configurations qui font gagner un temps fou sur ce type de debug.

L’angle mort de cette technique reste la granularité. Viewport-Width donne une largeur, pas la densité de pixels ni le type de périphérique. Mais pour l’immense majorité des optimisations de performance, c’est suffisant. On ne cherche plus à savoir si on a affaire à un iPhone 17 ou à un Pixel 11, on veut savoir si l’écran fait moins de 600 px de large et si la connexion est en 3G. Ce changement de paradigme oriente les décisions vers les contraintes réelles, pas vers le marketing des noms de modèles.

Cas réel : un e-commerce récupère 38 % de crawl en supprimant son sniffing Next.js

Prenez un site de prêt-à-porter, Next.js 14, App Router, déployé sur Vercel. L’équipe avait écrit un middleware qui scindait le rendu en deux variantes : une page « desktop » avec carrousel JS lourd et une page « mobile » allégée, sans carrousel et avec un menu hamburger statique. La détection s’appuyait sur une regex User-Agent visant Mobile|Android|iPhone. En apparence inoffensif.

Les journaux de crawl ont révélé le drame. Googlebot mobile recevait la version allégée, qui omettait les données structurées JSON-LD intégrées au carrousel desktop. Les balises de prix, les URLs canoniques et les images principales disparaissaient du DOM servi au bot. Le taux d’indexation a plongé de 40 % en trois semaines. Dans le même temps, le LCP moyen sur mobile grimpait à 4,1 secondes parce que le CDN devait stocker et servir deux versions distinctes, et que le taux de hit du cache frôlait les 30 %.

La correction a tenu en deux pull requests. La première a supprimé le middleware, rendant le rendu uniforme. La seconde a introduit un composant client qui détecte window.innerWidth et charge dynamiquement le carrousel uniquement au-dessus de 768 px, avec une suspension lazy-loadée. Résultat mesuré sous quinze jours : 38 % d’URLs supplémentaires indexées, LCP mobile ramené à 2,1 secondes, TTFB stabilisé à 65 ms. Sans changer une seule balise meta, le simple fait de ne plus fragmenter le contenu par UA a restauré le signal d’indexation.

Adopter la détection de capacités sans exploser son planning

Basculer ne demande pas une refonte. On peut avancer par couches.

La première consiste à auditer les middlewares et les fonctions serveur pour identifier toute lecture du header User-Agent. Un simple grep sur le dépôt suffit. Si une condition userAgent.includes ou une regex surgit, on la marque comme dette technique. La deuxième couche remplace ces points par des en-têtes non identifiants quand c’est possible (Save-Data, Viewport-Width) ou par une détection purement graphique côté client (matchMedia, ResizeObserver). La troisième couche encapsule la logique de feature detection dans un module dédié qu’on importe uniquement côté client, avec un fallback statique servi universellement.

Souvent, les équipes s’inquiètent de l’impact SEO d’une page servie de façon identique à tous. L’argument tient rarement l’examen des logs. La version uniforme contient tout le contenu, toutes les données structurées, tous les liens internes. C’est précisément ce que Googlebot veut consommer. Les ajustements cosmétiques (taille de typographie, disposition des blocs) sont gérés en CSS media queries, qui n’affectent ni le DOM ni l’indexation.

Ce travail d’audit se marie naturellement avec une revue de vos Core Web Vitals. Un site qui élimine ses variations par UA réduit mécaniquement le nombre de layouts à tester, simplifie le profiling de performance et rend les optimisations de LCP ou d’INP reproductibles sur tous les segments de trafic.

Questions fréquentes

Puis-je continuer à utiliser le header User-Agent pour des statistiques serveur sans impact SEO ?

Oui, si la lecture se limite à des logs internes et ne conditionne jamais le HTML servi. Dès que l’UA pilote une branche de rendu, le risque de cloaking existe. Stockez l’UA dans un outil d’analyse, sans l’utiliser pour modifier la réponse HTTP.

La détection de fonctionnalités côté client ajoute-t-elle un flash de contenu non stylisé (FOUC) ?

Pas si le script est placé en inline dans le <head> et qu’il manipule une classe sur <html> avant le premier rendu. Le navigateur bloque l’affichage le temps d’exécuter ce micro-script synchrone. Avec un payload de quelques centaines d’octets, le délai est imperceptible et empêche tout FOUC visible.

Une edge function qui lit Save-Data peut-elle casser la mise en cache CDN ?

L’en-tête Save-Data fait partie des en-têtes de négociation de contenu recommandés. Les CDN modernes le gèrent sans démultiplier les entrées de cache de façon incontrôlable, contrairement à User-Agent. Testez toujours le taux de hit avant de généraliser, mais la fragmentation est incomparablement plus faible.

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.