optimisation core web vitals 8 min

RWD server-side profiling : l'angle mort des audits de performance

Le profiling côté serveur révèle les goulets d'étranglement que Lighthouse ignore. Instrumentez vos logs pour réduire le TTFB mobile et améliorer le LCP.

Par Julien Morel
Partager

On a passé quarante-huit heures à débuguer un LCP mobile à 4,8 secondes sur une fiche produit. Lighthouse ne montrait rien d’anormal côté front : les images étaient en fetchpriority="high", le CSS critique inliné, le JS asynchrone. C’est dans les logs serveur qu’on a trouvé le coupable. Une requête SQL non filtrée par device ramenait 12 ko de données de recommandation que le DOM desktop exploitait, mais que le breakpoint mobile masquait via un display: none. Le serveur la calculait, la sérialisait, l’envoyait, et le navigateur mobile la chargeait quand même, alourdissant le TTFB de 800 ms sur un réseau 4G moyen. Cet article part de ce constat : le responsive design ne se joue pas dans la feuille de styles, il se profile dans les logs du serveur.

Pourquoi le TTFB global cache une réalité mobile dégradée

Quand tu regardes la moyenne TTFB dans ta console d’hébergement ou dans la Search Console, tu vois une valeur unique, lissée. Sur un site e-commerce qui fait 60 % de desktop et 40 % de mobile, la moyenne efface les extrêmes. Un TTFB de 220 ms en moyenne peut cacher 140 ms sur desktop et 380 ms sur mobile. Pire, si ton trafic mobile est plus faible la nuit, la moyenne journalière s’en trouve encore plus trompée.

Le profiling serveur sans distinction de device est un thermomètre qui prend la température dans une pièce, mais jamais à hauteur du sol. Googlebot smartphone, lui, mesure ton TTFB mobile, pas ta moyenne globale. C’est cette valeur qui compte pour l’évaluation des Core Web Vitals en mobile-first. Si tu ne dissocies pas les métriques par user-agent, tu optimises dans le brouillard.

La raison est simple : les temps de réponse serveur dépendent de la charge CPU mobilisée pour construire la page. Or, la logique métier qui peuple un panier, qui hydrate une barre latérale desktop-only, qui assemble des données de recommandation pour un carrousel visible uniquement au-dessus de 1024 px, cette logique s’exécute pour tous les devices si tu ne la conditionnes pas. Sur mobile, elle consomme du temps processeur pour du DOM qui atterrira masqué. Le navigateur, lui, devra parser ce DOM invisible et télécharger les assets associés. Le TTFB s’allonge, le LCP aussi, et Lighthouse ne te le dira jamais.

Ce que le profiling serveur expose que les audits frontaux ignorent

Lighthouse, WebPageTest, CrUX : ces outils mesurent le résultat, pas l’origine. Ils te disent que le LCP est à 3,2 secondes, que le TTFB est de 450 ms, que le First Paint tarde. Mais ils ne te montrent pas pourquoi le serveur a mis 450 ms à générer le premier octet. C’est la boîte noire entre la requête HTTP et le début du flux HTML.

Le profiling serveur, lui, ouvre cette boîte. Il trace l’exécution du code métier, des accès base de données, des appels API externes, de la sérialisation JSON, de la construction du template. Et il le fait avec une granularité qui permet de filtrer par User-Agent. Résultat : tu découvres que la route /produit/123 exécute 27 requêtes SQL dont 4 seulement sont utiles au viewport mobile, que le service de personnalisation appelle une API tierce qui répond en 600 ms sur mobile (parce que l’Edge n’est pas le même), ou que la sérialisation du state initial pour l’hydratation React pèse 18 ko inutiles sur un téléphone moyen de gamme.

Personne ne te dira ça dans un rapport Lighthouse. C’est pourtant la différence entre un site qui passe les seuils Core Web Vitals sur lab data et un site qui les rate en condition réelle. Le profiling serveur ne remplace pas les audits frontaux, il les complète en amont, là où le DOM n’existe pas encore.

Instrumenter son serveur Node.js pour capturer l’empreinte device

Si tu tournes sous Node.js, tu disposes d’outils natifs pour attaquer ce problème sans dépendance lourde. L’API performance.now() et les AsyncLocalStorage permettent de poser des marqueurs autour de chaque étape de rendu, et de les taguer avec le user-agent de la requête entrante.

Un middleware minimal suffit pour commencer :

app.use((req, res, next) => {
  const ua = req.get('User-Agent') || '';
  const device = /Mobile|Android|iPhone/.test(ua) ? 'mobile' : 'desktop';
  req.device = device;
  req.profile = { device, steps: [] };
  const start = performance.now();
  res.on('finish', () => {
    req.profile.totalMs = Math.round(performance.now() - start);
    // log structuré, à envoyer vers ta stack de monitoring
    console.log(JSON.stringify(req.profile));
  });
  next();
});

Ensuite, dans tes contrôleurs ou résolveurs, tu peux ajouter des étapes simplement :

req.profile.steps.push({ name: 'db-products', ms: Date.now() - stepStart });

L’idée n’est pas d’instrumenter chaque ligne mais de segmenter le chemin critique : résolution du template, fetch de données, appels API, sérialisation. En agrégeant ces logs par device, tu obtiens une distribution du temps passé par étape. Sur un de nos bancs d’essai, cette instrumentation a révélé que la recherche de prix en base prenait 60 ms sur desktop et 180 ms sur mobile. La différence ? Un index de base de données ignorait une colonne de segmentation régionale, et les requêtes mobile atterrissaient sur un serveur de base plus éloigné. Lighthouse n’avait aucun moyen de le voir.

💡 Conseil : Stocke ces profils dans un format structuré (JSON en log drain) plutôt que des chaînes concaténées. Tu pourras les requêter par plage de temps, device, route, et croiser avec les données RUM.

Le piège des redirections mobiles et du Vary: User-Agent

Un classique qu’on voit encore sur des migrations récentes : le site renvoie une 302 vers une URL mobile séparée quand il détecte un smartphone. Outre les problèmes d’indexation canonique, cette redirection détruit le TTFB mobile. Le navigateur doit résoudre une première requête, recevoir une 302, puis relancer une seconde requête. À chaque fois, la connexion TLS peut être renégociée, le DNS peut être résolu.

GET /produit/123
302 Found
Location: /m/produit/123

En condition mobile, cette séquence ajoute facilement 400 à 800 ms avant même que le serveur ne commence à générer du HTML. Le profiling serveur le détecte : le premier log de route apparaît avec un totalMs très bas, mais le second log pour /m/... affiche un TTFB additionnel. Pourtant, les audits frontaux voient juste la page finale.

Le Vary: User-Agent pose un problème plus sournois. Il indique aux caches HTTP que la réponse varie selon l’en-tête. Si ton CDN ou ton reverse proxy n’est pas configuré pour stocker plusieurs versions par URL, tu perds tout bénéfice du cache pour les devices minoritaires. Le profiling serveur peut révéler un taux de cache HIT proche de zéro sur mobile alors qu’il est normal sur desktop, simplement parce que le cache ne stockait que la version desktop. La solution n’est pas de supprimer le Vary mais d’adapter la clé de cache ou d’utiliser un edge capable de normaliser le user-agent par catégorie.

Adapter la charge avant le rendu : un vrai responsive côté serveur

Une fois les métriques mobile isolées, l’étape suivante est d’agir sur le serveur pour réduire le travail inutile. On ne parle pas de servir deux sites distincts, mais de conditionner la logique métier au device avant même de construire le HTML.

Sur une page listing avec filtres, un serveur classique va chercher les 50 produits, leurs images, leurs avis, leurs variantes, puis le template va masquer les colonnes secondaires via CSS. En profiling, on voit que l’appel base prend 120 ms et que la sérialisation du state pour React pèse 22 ko de JSON inline dans le <script> d’hydratation. Sur mobile, ces 22 ko sont inutiles si le filtre latéral n’est pas affiché. En adaptant le contrôleur, on peut réduire l’appel base à 25 produits sans variantes, et réduire le state initial à 8 ko. Résultat : TTFB mobile réduit de 35 %, LCP amélioré de 800 ms.

La technique n’est pas gratuite. Elle impose de maintenir une condition par device dans chaque contrôleur concerné. Mais c’est du code métier, pas du hack CSS, et ça se teste unitairement. Certains frameworks encouragent cette approche : Next.js avec le middleware permet de lire le user-agent et de choisir un rendu allégé, Remix expose les headers dans les loaders. Quand on profile du Node.js en local, le choix de l’IDE influe sur la vitesse de debug. Nous avons comparé Claude Code et Cursor IDE pour ce type de navigation dans une base de code qui mêle logique serveur et templates.

L’optimisation ne concerne pas que les données. Les polices, les bundles JS, les CSS additionnels peuvent être chargés conditionnellement côté serveur avant envoi du HTML. Plutôt que d’envoyer un link rel="preload" pour une typo desktop de 80 ko, tu l’omets quand le device est un smartphone. Résultat immédiat sur le LCP et le bilan carbone.

Le poids du state management dans le temps serveur

Un autre tueur silencieux que seul le profiling serveur met en lumière : la sérialisation du state global. Avec une librairie de state management côté client, on a tendance à peupler un store initial depuis le serveur pour éviter un flash de chargement. Ce store inclut parfois des états d’UI, des préférences, des données de session qui ne servent qu’au desktop. La sérialisation d’un objet JavaScript profond en JSON, puis son injection dans le HTML, peut ajouter 40 à 80 ms de temps CPU serveur sur un volume conséquent. Le profiling montre un pic dans la fonction JSON.stringify au moment du rendu.

Si votre application React utilise un store global, il est probable que le serveur envoie bien plus d’état que nécessaire au mobile. C’est là que l’isolation par device prend tout son sens : on ne garde dans le state initial que les tranches pertinentes pour le viewport détecté. Zustand, par exemple, facilite cette segmentation avec des stores multiples. Nous avons détaillé l’impact de cette approche dans notre retour sur le state management avec Zustand. Le profiling serveur devient alors l’outil qui mesure l’effet de ces choix architecturaux sur le TTFB, avant même le premier render.

Questions fréquentes

Est-ce que le profiling serveur peut aider à l’indexation mobile-first ?

Oui, indirectement. Googlebot smartphone utilise un réseau mobile simulé et un CPU throttling. Si le serveur met trop de temps à répondre, le budget de crawl de la page se réduit, et le rendu peut être interrompu. Le profiling serveur permet de s’assurer que le TTFB vu par Googlebot reste sous les 500 ms, ce qui maximise la probabilité d’un rendu complet et d’une indexation fidèle du contenu.

Quelle différence avec le Real User Monitoring (RUM) ?

Le RUM mesure le TTFB, le LCP, l’INP depuis le navigateur réel. Il te confirme le problème, mais il n’explique pas la cause côté serveur. Le profiling serveur complète le RUM en traçant le chemin d’exécution. L’un te dit « le TTFB mobile est de 600 ms », l’autre te dit « ces 600 ms se décomposent en 200 ms de base de données, 150 ms d’API externe, 100 ms de sérialisation ». Les deux sont nécessaires.

Peut-on profiler en production sans impacter les performances ?

Oui, à condition d’utiliser un échantillonnage. Instrumenter 1 à 5 % des requêtes suffit à obtenir une vision statistiquement fiable sans alourdir le serveur. Le surcoût d’un performance.now() est négligeable. Le risque, c’est l’accumulation de logs, pas le ralentissement. On peut aussi profiler en continu mais n’envoyer les traces que lorsque le temps total dépasse un seuil (par exemple 300 ms) pour concentrer l’effort sur les cas lents.

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.