RUM des Core Web Vitals en 2026 : Collecter et Analyser les Métriques Terrain avec web-vitals.js
Le RUM Core Web Vitals capture ce que vivent vos vrais utilisateurs, pas ce que Lighthouse simule. Ce guide 2026 montre comment instrumenter web-vitals.js v5, envoyer via sendBeacon, segmenter les percentiles p75/p95 et diagnostiquer LCP et INP.
Le Real User Monitoring (RUM) des Core Web Vitals consiste à collecter les métriques de performance directement dans le navigateur de vos utilisateurs réels, puis à les envoyer à un backend d'analyse pour observer les percentiles (p75, p95) par classe d'appareil, type de réseau et route. Contrairement aux tests synthétiques Lighthouse qui simulent un environnement contrôlé, le RUM capture la vérité du terrain : ce que voient vos vrais visiteurs, sur leurs vrais téléphones, sur leurs vraies connexions 4G instables. En 2026, la bibliothèque officielle web-vitals v5 rend cette collecte triviale (à condition, bien sûr, de l'instrumenter correctement).
Le RUM mesure LCP, INP, CLS, FCP et TTFB depuis les navigateurs de vos utilisateurs réels, tandis que Lighthouse ne simule qu'un scénario unique dans un environnement contrôlé.
La bibliothèque web-vitals v5 (2026) expose onLCP, onINP, onCLS, onFCP et onTTFB avec des attributions détaillées pour identifier l'élément fautif.
L'agrégation doit se faire au 75e percentile (p75) séparément mobile/desktop. Les moyennes masquent les queues de distribution où se cachent les problèmes réels.
CrUX fournit des données publiques agrégées sur 28 jours, mais ne segmente pas par route ni par utilisateur : votre RUM comble ce vide.
Envoyez les métriques via navigator.sendBeacon() sur l'événement visibilitychange pour ne perdre aucune donnée quand l'utilisateur ferme l'onglet.
Une couche RUM complète en 2026 coûte moins de 3 Ko gzip et 0 ms de blocage du thread principal si vous la chargez correctement.
RUM vs monitoring synthétique : la différence qui compte
Franchement, si vous prenez une décision de perf en vous appuyant uniquement sur un score Lighthouse, vous décidez à l'aveugle. Les tests synthétiques exécutent votre site dans une machine virtuelle avec un CPU throttlé au 4x, un réseau simulé 4G Slow (150 ms RTT, 1,6 Mbps), et une seule route à la fois. Vos utilisateurs réels, eux, tournent sur des Galaxy A13 de 2022 avec des CPU 8x plus lents, des connexions 3G perdues dans le métro, des extensions qui injectent 400 Ko de JavaScript, et des onglets d'arrière-plan qui volent des cycles CPU.
Le Real User Monitoring capture cette réalité. Chaque visite envoie un échantillon de mesures (LCP, INP, CLS, FCP, TTFB) que vous agrégez côté serveur en percentiles. Le p75 est la métrique canonique de Google : c'est la valeur qui définit si vous passez les seuils Core Web Vitals dans le rapport CrUX, et donc dans Google Search Console. Un p75 de LCP à 2,4 secondes signifie que 75 % de vos utilisateurs voient le plus grand élément apparaître en moins de 2,4 secondes. Les 25 % restants, souvent les visites les plus précieuses commercialement, attendent davantage.
Le RUM n'élimine pas le synthétique pour autant. Lighthouse reste utile pour la régression pré-production, pour comparer deux implémentations dans un environnement stable, et pour alimenter les revues de code. Mais pour piloter la stratégie, seul le terrain compte. J'ai vu des équipes passer trois sprints à optimiser le LCP synthétique de 3,1 s à 1,8 s ; le p75 terrain n'a pas bougé d'un poil, parce que le vrai goulet d'étranglement était un tracker publicitaire qui ne se déclenchait même pas dans Lighthouse.
La bibliothèque web-vitals.js en 2026 (v5)
La bibliothèque web-vitals maintenue par l'équipe Chrome est le standard de facto pour instrumenter le RUM. La version 5, publiée début 2026, apporte trois évolutions importantes : le module d'attribution est plus détaillé (identification du sous-composant fautif pour INP), le calcul du CLS gère correctement les nouvelles animations View Transitions, et le poids gzip est descendu à 2,1 Ko pour le bundle standard (1,3 Ko pour le noyau seul).
Les cinq métriques exposées correspondent aux Core Web Vitals officiels et à leurs métriques de diagnostic :
onLCP(callback) : Largest Contentful Paint, seuil « bon » sous 2,5 s.
onINP(callback) : Interaction to Next Paint, remplaçant officiel de FID depuis mars 2024, seuil « bon » sous 200 ms.
onFCP(callback) : First Contentful Paint, métrique de diagnostic.
onTTFB(callback) : Time to First Byte, métrique de diagnostic serveur.
Chaque callback est appelé une seule fois par page, généralement au moment où l'utilisateur quitte l'onglet (pour INP et CLS, qui peuvent évoluer pendant la session). C'est le comportement souhaité : vous voulez la valeur finale, pas des mises à jour incrémentales.
Installer et instrumenter web-vitals dans votre site
L'installation prend deux commandes. J'utilise pnpm ici, mais npm ou yarn fonctionnent identiquement :
pnpm add web-vitals
Pour l'instrumentation la plus légère (juste envoyer les cinq métriques, sans attribution), créez un module dédié que vous chargez en fin de <body> ou via un import dynamique après l'hydratation :
Chargez ce module tardivement pour ne pas polluer le TTI. Si vous êtes sur Next.js, mettez-le dans app/rum-client.tsx comme composant client marqué 'use client' et rendez-le depuis le layout racine. Sur Vite/React, placez l'import dans main.tsx après createRoot(...). Le code s'exécute une seule fois par navigation et n'ajoute aucun blocage rendu.
Envoyer les métriques à un backend d'analyse
Le choix entre navigator.sendBeacon() et fetch(..., { keepalive: true }) n'est pas cosmétique. sendBeacon est explicitement conçu pour ce cas : le navigateur garantit la livraison même si l'utilisateur ferme immédiatement l'onglet, et la requête n'attend pas de réponse. C'est ce que la bibliothèque recommande comme comportement par défaut depuis la v3.
Côté backend, votre endpoint doit être ultra-rapide (moins de 50 ms de traitement) et retourner un 204 No Content vide. Pas de logique métier, pas de jointure : vous accumulez les événements bruts dans une table ou un stream (Kafka, Kinesis, ClickHouse, BigQuery streaming inserts), puis vous agrégez en asynchrone. Un exemple minimal en Node.js avec Fastify :
La classification par classe d'appareil (low-end, mid-tier, high-end) est celle que j'utilise en production. Elle s'appuie sur navigator.deviceMemory et navigator.hardwareConcurrency, deux API disponibles dans tous les navigateurs modernes. C'est grossier (un Pixel 4a et un Galaxy S10 se retrouvent dans le même bucket), mais c'est suffisant pour comparer des cohortes.
Comment envoyer les Core Web Vitals à Google Analytics 4 ?
Si vous n'avez pas d'infrastructure de collecte dédiée, Google Analytics 4 accepte les Core Web Vitals comme événements custom. La bibliothèque web-vitals s'intègre parfaitement avec gtag(). Voici l'implémentation recommandée par l'équipe Chrome :
import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals';
function sendToGA4(metric) {
gtag('event', metric.name, {
// Les valeurs GA4 doivent etre des entiers
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
metric_id: metric.id,
metric_value: metric.value,
metric_delta: metric.delta,
metric_rating: metric.rating,
non_interaction: true, // ne perturbe pas le bounce rate
});
}
onCLS(sendToGA4);
onFCP(sendToGA4);
onINP(sendToGA4);
onLCP(sendToGA4);
onTTFB(sendToGA4);
Deux subtilités importantes. D'abord, GA4 stocke les valeurs sous forme d'entiers ; le CLS étant une valeur décimale (0,05 par exemple), on le multiplie par 1000 pour préserver la précision. Ensuite, le paramètre non_interaction: true évite que ces événements automatiques soient comptés comme des interactions utilisateur (sinon, votre taux de rebond s'effondre artificiellement).
Dans GA4, créez une exploration « Free Form » avec dimension = event_name, métrique = Average of metric_value, filtre = event_name matches regex ^(LCP|INP|CLS|FCP|TTFB)$. Pour obtenir un vrai p75, exportez vers BigQuery : GA4 ne calcule pas les percentiles nativement dans l'interface.
Segmenter les percentiles par classe d'appareil et réseau
La moyenne est un mensonge en performance web. Si votre AVG(LCP) est de 2,3 s, cela signifie que la moitié de vos utilisateurs voient un LCP inférieur à cette valeur. Mais l'autre moitié peut expérimenter 6 secondes. Le p75 est le standard de Google. Personnellement, je surveille aussi p95 et p99, parce que c'est là que se cachent les incidents. Une requête ClickHouse type ressemble à ceci :
SELECT
device_class,
connection,
quantile(0.75)(value) AS p75,
quantile(0.95)(value) AS p95,
quantile(0.99)(value) AS p99,
count() AS samples
FROM rum_events
WHERE metric = 'LCP'
AND created_at >= now() - INTERVAL 28 DAY
AND url = '/produits/checkout'
GROUP BY device_class, connection
HAVING samples > 100
ORDER BY p75 DESC;
La fenêtre glissante de 28 jours reproduit celle utilisée par CrUX, ce qui facilite les comparaisons. Le filtre HAVING samples > 100 évite les segments à faible volume où les percentiles sont statistiquement bruités. Sur des routes à faible trafic, remontez à samples > 500 ou étendez la fenêtre à 90 jours.
La segmentation par route est souvent plus révélatrice que la segmentation par appareil. J'ai vu des sites où la home affichait un p75 LCP à 1,9 s (excellent) et la page produit à 4,7 s (catastrophique), parce qu'un slider d'images non lazy-loadé chargeait 12 hero images en parallèle. Sans segmentation par route, le p75 global à 2,4 s aurait masqué le problème. Notre guide d'optimisation des images web en AVIF et WebP détaille comment corriger ce type de pattern.
Différence entre CrUX et un RUM maison ?
Le Chrome UX Report (CrUX) est le RUM public de Google : les navigateurs Chrome des utilisateurs qui ont opté pour la synchronisation envoient anonymement leurs Core Web Vitals à Google, qui agrège par origine et publie des snapshots mensuels et journaliers (via l'API CrUX History). C'est cette donnée qui alimente le rapport Core Web Vitals de la Search Console, et qui sert de signal de classement.
Dimension
CrUX
RUM maison (web-vitals.js)
Source de données
Chrome uniquement (utilisateurs opt-in)
Tous les navigateurs qui exécutent votre JS
Granularité
Par origine (parfois par URL top-trafic)
Par route, cohorte, expérience A/B, feature flag
Fraîcheur
28 jours glissants, publication quotidienne
Temps réel (secondes à minutes)
Utilisé par Search Console
Oui (signal de classement)
Non (usage interne)
Segmentation appareil
Mobile / Desktop uniquement
Illimitée (mémoire, CPU, réseau, géo, A/B)
Coût
Gratuit
Infrastructure + stockage
Volume nécessaire
Seuil de trafic minimum (~100 samples/URL)
Aucun minimum
Ma règle : CrUX pour le signal officiel qui compte pour le SEO, RUM maison pour le pilotage produit. Les deux ne se contredisent pas, ils mesurent la même chose à des échelles différentes. Quand ils divergent significativement (plus de 15 % d'écart sur le p75), c'est généralement que votre RUM échantillonne différemment (Safari inclus, bots exclus, etc.). Réconciliez toujours les deux avant de trancher.
Attribution : identifier l'élément qui casse votre LCP ou INP
Savoir que votre p75 INP est à 340 ms ne vous dit pas quoi corriger. C'est là qu'intervient le module d'attribution de web-vitals v5, importé depuis un sous-chemin dédié :
Le sous-timing INP processingDuration vs presentationDelay est diagnostique. Un processingDuration élevé pointe vers un handler JavaScript trop lourd (mettez-le en startTransition React 19, ou dans un requestIdleCallback). Un presentationDelay élevé indique que le rendu suivant est bloqué par autre chose, souvent un forced synchronous layout ou un long task d'un tracker. Pour approfondir ce diagnostic, consultez notre guide d'optimisation INP en 2026.
L'attribution ajoute environ 800 octets gzip au bundle. Sur des sites à fort trafic, je recommande de l'activer uniquement pour un échantillon (10 à 20 % des sessions) pour ne pas surcharger votre pipeline d'ingestion, tout en conservant assez de données pour le debug. J'ai personnellement pris cette leçon en shipant un dashboard qui explosait à 5 millions d'événements/jour ; l'échantillonnage a divisé les coûts par cinq sans perte d'insight.
Dashboards, alertes et budgets de performance
Un pipeline RUM sans dashboard ni alerte, c'est un rack de disques dur. Trois vues sont indispensables : la vue temps réel (dernière heure, granularité minute) pour détecter les régressions de déploiement, la vue tendance (28 jours glissants, granularité jour) pour piloter les objectifs trimestriels, et la vue segmentée (par route × device × connexion) pour l'analyse root cause.
Configurez des alertes Slack ou PagerDuty sur trois conditions : (1) le p75 d'une métrique CWV dépasse le seuil « good » pendant plus de 30 minutes consécutives, (2) le volume de samples chute de plus de 50 % en 15 minutes (signal que votre RUM lui-même est cassé), (3) régression relative de plus de 20 % vs la même heure la semaine précédente. Les seuils absolus ratent les régressions qui restent sous le budget ; les seuils relatifs sont plus sensibles.
Quelle est la différence entre RUM et monitoring synthétique ?
Le RUM (Real User Monitoring) collecte les métriques dans le navigateur de vos utilisateurs réels, en production. Le monitoring synthétique (Lighthouse, WebPageTest) exécute votre site dans un environnement simulé contrôlé. Le RUM reflète la vérité du terrain avec toute sa variabilité ; le synthétique offre la reproductibilité nécessaire pour la régression pré-production. Utilisez les deux, pas l'un ou l'autre.
La bibliothèque web-vitals affecte-t-elle les performances de mon site ?
Non, à condition de la charger correctement. Le poids gzip est de 2,1 Ko pour la v5 standard et le code s'exécute uniquement dans des callbacks asynchrones enregistrés via PerformanceObserver. Aucun blocage du thread principal, aucun impact mesurable sur LCP ou INP si l'import se fait après l'hydratation ou en type="module" avec async.
Pourquoi mes valeurs RUM diffèrent-elles de mes scores Lighthouse ?
Parce que Lighthouse mesure une seule visite simulée dans un environnement contrôlé (CPU throttlé 4x, réseau 4G Slow) sur une machine data-center, tandis que votre RUM agrège des milliers de visites réelles sur des appareils et réseaux variés. Un écart de 30 à 50 % entre les deux est normal. Le p75 RUM est la vérité qui compte pour vos utilisateurs et pour le SEO.
Quel percentile utiliser pour les Core Web Vitals ?
Le p75 (75e percentile) est le standard officiel de Google : c'est la valeur utilisée par CrUX et par Search Console pour déterminer si vous passez les seuils « Good ». Surveillez également le p95 et p99 en interne pour capturer les cas extrêmes qui affectent une minorité, mais très perceptible, de vos utilisateurs, notamment sur les appareils bas de gamme.
Combien de temps conserver les données RUM brutes ?
Trente jours de données brutes suffisent pour reproduire la fenêtre glissante CrUX et faire du debug incidents. Au-delà, agrégez en résumés quotidiens (p75, p95, count par route × device × connexion) et supprimez les événements individuels. Cela réduit les coûts de stockage de 95 % et respecte les principes de minimisation RGPD.
Configurez la Speculation Rules API pour prerendrer les pages et offrir des navigations sous 100 ms : prefetch, eagerness, document rules et View Transitions.
L'INP mesure la latence entre une interaction et le prochain rendu. Pour passer sous 200 ms en 2026, maîtrisez Long Animation Frames, scheduler.yield, useTransition et les Web Workers. Guide pratique avec code et workflow d'audit.
Réduisez votre bundle JS avec Vite en 2026. Diagnostic avec rollup-plugin-visualizer, code splitting, tree shaking, remplacement des dépendances lourdes, compression Brotli et budget de performance CI avec du code prêt à l'emploi.