Un site e-commerce de prêt-à-porter qu’on audite ce printemps affiche un INP de 340 ms sur un Moto G4. Dans les traces, le thread principal passe 120 ms à exécuter un script de feature detection de 18 Ko, avant même que le moindre gestionnaire d’événement métier ne soit attaché. Le plus absurde : ce script tourne de la même façon sur un Chrome 110 flambant neuf et sur un navigateur hybride de 2018. La solution directe, c’est de basculer la détection de capacités côté serveur, avant le rendu. Reste à éviter les pièges qui cassent ton crawl ou ton cache.

La feature detection, un anti-pattern de performance en 2026

Le principe de la feature detection est sain : on teste si le navigateur supporte IntersectionObserver, WebP, loading="lazy", avant d’activer un comportement. Mais dans la pratique, ces vérifications consomment du temps CPU en pleine phase d’évaluation critique. Pire, elles ajoutent des kilo-octets de code mort pour tous les navigateurs modernes qui disposent déjà de ces fonctionnalités.

Sur mobile, l’impact est immédiat. Chaque milliseconde de parse et d’exécution JavaScript repousse l’instant où la page répond au premier clic. Quand on supprime 15 Ko de vérifications redondantes, on ne gagne pas seulement du poids réseau : on libère le thread principal au moment où l’utilisateur commence à interagir. Le gain se voit directement dans les rapports d’optimisation core web vitals, où l’INP chute parfois de 60 ms sans toucher à la logique métier.

À cela s’ajoute un problème de maintenabilité. La feature detection éparpillée dans des dizaines de composants finit par ne plus correspondre aux navigateurs réellement utilisés. On maintient des polyfills pour IE11 alors que le trafic de ce navigateur est tombé à zéro depuis trois ans. La charge mentale pour l’équipe front ne disparaît jamais, parce que chaque nouvelle fonctionnalité entraîne un test conditionnel supplémentaire.

Les headers HTTP suffisent presque toujours

!A close-up angled view of a stack of printed HTTP header documents on a metallic server rack shelf, red and green LED li

Le navigateur envoie User-Agent, Accept-CH, Save-Data. Le serveur peut déduire le support du WebP, du lazy-loading natif, du format d’image AVIF. Pas besoin de script. Un simple if dans le middleware, et le bundle servi ne contient plus le code inutile. La décision est prise avant le premier octet HTML.

Vary et CDN : le diable est dans l’en-tête

Servir un contenu différent selon le User-Agent expose à un piège de cache. Si le CDN ignore la variation, un utilisateur sous Chrome récupère une page pensée pour un vieux Safari. À l’inverse, si on active Vary: User-Agent sans précaution, on pulvérise le taux de hit parce que les agents utilisateurs sont trop nombreux. Fastly et Cloudflare proposent des normalisations d’en-têtes qui regroupent les familles de navigateurs en quelques variantes stables. On obtient ainsi un cache encore chaud sur la quasi-totalité des requêtes.

Il faut aussi se méfier des régressions silencieuses. Un changement de règle de détection peut invalider le cache et provoquer un pic de charge sur l’origine. On verrouille la normalisation dans la configuration CDN, et on ne la modifie jamais sans un déploiement progressif. Une erreur qu’on voit souvent : ajouter une signature de bot d’indexation dans la clé de cache. Si Googlebot ne trouve pas exactement le même contenu qu’un Chrome récent, il finit par soupçonner un cloaking, même si l’intention était bonne.

⚠️ Attention : ne jamais faire varier le contenu HTML pour Googlebot mobile par rapport à un vrai utilisateur mobile sur la seule base de l’en-tête. Les systèmes de classement savent comparer les rendus et y verront une tentative de manipulation.

Ce que Googlebot voit, et ce qu’il ne doit pas voir

!A robotic spider silhouette crawling across a glowing computer monitor screen, thin legs partially obscuring lines of co

Googlebot mobile utilise un agent utilisateur spécifique, mais il rend la page avec un Chrome headless récent. Autrement dit, il est capable d’exécuter du JavaScript et de montrer le même résultat qu’un visiteur. Si vous lui servez une version allégée en features alors que l’utilisateur reçoit une version enrichie, l’écart se verra tôt ou tard dans l’indexation. À l’inverse, si vous le traitez comme un navigateur moderne standard, il n’y a pas de problème.

Ce qu’il faut éviter, c’est de tomber dans le piège inverse : servir à Googlebot une version « dégradée » par peur d’un budget de crawl. Le crawl budget n’a rien à voir avec le poids du JavaScript. Googlebot n’interrompt pas l’exploration d’un site parce que la page contient 200 Ko de JS. En revanche, il peut être ralenti si ces scripts mettent 15 secondes à se stabiliser. On garde donc le même HTML pour Googlebot et pour Chrome, et on laisse la détection serveur alléger discrètement les ressources sans toucher au contenu visible.

Garder un filet côté client sur quelques capacités

La détection serveur ne couvre pas tout. Accès au presse-papiers, reconnaissance vocale, formats d’image exotiques : ces capacités ne se devinent pas dans les en-têtes. Et un profil serveur peut se tromper quand un navigateur masque son User-Agent.

On réduit alors la feature detection à quelques lignes, en priorité basse, pour les navigateurs minoritaires uniquement. Si vous gardez un état de capacités détectées pour des composants dynamiques, une bibliothèque comme Zustand pour le state management React évite les rendus en cascade, sans supprimer le coût initial de la détection. On la garde au strict minimum.

Un middleware Next.js pour trancher avant le rendu

Banc d’essai : un Next.js 14. Dans le middleware, on lit User-Agent et Save-Data. On en déduit un profil : mobile lent, mobile rapide, desktop. Ce profil est stocké dans le contexte de la requête et injecté via l’en-tête Accept-CH dès la réponse. Résultat : les images sont servies en WebP ou AVIF selon la capacité réelle, et le bundle JavaScript exclut les polyfills pour les navigateurs morts.

Le middleware a été rédigé en pair-programming avec un LLM, l’occasion de comparer Claude Code face à Cursor sur ce type de tâche. Une fois en production, le code a supprimé 14 Ko de JavaScript incompressé sur mobile et fait disparaître la plupart des tests conditionnels côté client. Le temps de blocage du thread principal lors d’un appui tactile est passé sous les 80 ms, là où il dépassait encore 200 ms avec l’ancien système de feature detection.

💡 Conseil : Utilisez Vary: User-Agent uniquement sur les ressources qui diffèrent vraiment. Dans Next.js, cela peut se gérer au niveau du CDN en isolant les pages dynamiques des assets statiques.

Questions fréquentes

La détection côté serveur fonctionne-t-elle avec le streaming SSR ?

Oui, à condition d’avoir déterminé les capacités avant de flusher le premier chunk. On stocke le profil dans le contexte de requête, ce qui évite un aller-retour serveur supplémentaire. Le framework lit ces informations juste avant le rendu et les transmet au flux.

Peut-on détecter le support des polices variables côté serveur ?

Pas directement via les en-têtes standards actuels, mais on peut utiliser une règle CSS @supports pour ajuster le chargement des polices sans alourdir le JavaScript. La détection serveur s’arrête là où le CSS prend le relais.

Est-ce que cela supprime totalement le besoin de <noscript> ?

Pas entièrement. Même si la version sans JavaScript servie par le serveur est propre, un contenu minimum dans <noscript> reste utile pour les crawlers qui n’exécutent pas le JavaScript. Cependant, la quantité de code à gérer diminue drastiquement.

Quiz personnalisé

Votre recommandation sur détection profil serveur

Quelques questions rapides pour adapter la recommandation à votre cas.

Q1 Votre situation sur détection profil serveur ?
Q2 Votre priorité ?
Q3 Votre horizon ?