Évaluer les performances de chargement sur le terrain à l'aide de Navigation Timing et Resource Timing

Découvrez les principes de base de l'utilisation des API Navigation et Resource Timing pour évaluer les performances de chargement sur le terrain.

Si vous avez utilisé la limitation de connexion dans le panneau "Réseau" des outils pour les développeurs d'un navigateur (ou dans Lighthouse dans Chrome) pour évaluer les performances de chargement, vous savez à quel point ces outils sont pratiques pour régler les performances. Vous pouvez mesurer rapidement l'impact des optimisations de performances grâce à une vitesse de connexion de référence constante et stable. Le seul problème est qu'il s'agit de tests synthétiques, qui génèrent des données de test, et non des données réelles.

Les tests synthétiques ne sont pas fondamentalement mauvais, mais ils ne sont pas représentatifs de la vitesse de chargement de votre site Web pour les utilisateurs réels. Cela nécessite des données de champ, que vous pouvez collecter à partir des API Navigation Timing et Resource Timing.

API permettant d'évaluer les performances de chargement sur le terrain

Navigation Timing et Resource Timing sont deux API similaires présentant un chevauchement important. Elles mesurent deux éléments distincts:

  • Le paramètre Navigation Timing (Temps de navigation) mesure la vitesse des requêtes de documents HTML (c'est-à-dire les requêtes de navigation).
  • Le temps de ressource mesure la vitesse des requêtes pour les ressources dépendantes des documents, telles que CSS, JavaScript, images, etc.

Ces API exposent leurs données dans un tampon d'entrée des performances, accessible dans le navigateur à l'aide de JavaScript. Il existe plusieurs façons d'interroger un tampon de performances, mais la plus courante consiste à utiliser performance.getEntriesByType:

// Get Navigation Timing entries:
performance.getEntriesByType('navigation');

// Get Resource Timing entries:
performance.getEntriesByType('resource');

performance.getEntriesByType accepte une chaîne décrivant le type des entrées que vous souhaitez récupérer du tampon d'entrée des performances. 'navigation' et 'resource' récupèrent les codes temporels des API Navigation Timing et Resource Timing, respectivement.

La quantité d'informations fournies par ces API peut paraître écrasante, mais elles sont essentielles pour mesurer les performances de chargement sur le terrain, car vous pouvez connaître ces délais auprès des utilisateurs lorsqu'ils visitent votre site Web.

Durée de vie et durée d'une requête réseau

La collecte et l'analyse de la navigation et de la durée des ressources sont un peu comme de l'archéologie, dans la mesure où l'on reconstitue après coup la courte durée d'une requête réseau. Parfois, il peut être utile de visualiser des concepts et, en cas de demande réseau, d'utiliser les outils pour les développeurs de votre navigateur.

Schéma des temps de mise en réseau, tel qu'illustré dans les outils pour les développeurs Chrome. Les codes temporels indiqués sont pour la mise en file d'attente des requêtes, la négociation de connexion, la requête elle-même et la réponse sous forme de barres de couleur.
Visualisation d'une requête réseau dans le panneau "Network" (Réseau) des outils de développement Chrome

Le cycle de vie d'une requête réseau comporte des phases distinctes, telles que la résolution DNS, l'établissement de la connexion, la négociation TLS, etc. Ces codes temporels sont représentés par un élément DOMHighResTimestamp. Selon votre navigateur, la précision des codes temporels peut être à la microseconde ou arrondie à la milliseconde. Examinons ces phases en détail, et leur lien avec le timing de navigation et le temps de ressource.

résolution DNS

Lorsqu'un utilisateur accède à une URL, le système de noms de domaine (DNS) est interrogé pour traduire un domaine en adresse IP. Ce processus peut prendre beaucoup de temps, voire prendre du temps sur le terrain. Le timing de navigation et le timing des ressources font référence à deux codes temporels liés au DNS:

  • domainLookupStart correspond au début de la résolution DNS.
  • domainLookupEnd correspond à la fin de la résolution DNS.

Il est possible de calculer le temps total de résolution DNS en soustrayant la métrique de début de la métrique de fin:

// Measuring DNS lookup time
const [pageNav] = performance.getEntriesByType('navigation');
const totalLookupTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;

Négociation de connexion

La négociation de la connexion, qui est un facteur de latence engendré par la connexion à un serveur Web, contribue également aux performances de chargement. Si le protocole HTTPS est utilisé, ce processus inclura également la date et l'heure de la négociation TLS. La phase de connexion comprend trois temps:

  • connectStart correspond au moment où le navigateur commence à ouvrir une connexion à un serveur Web.
  • secureConnectionStart marque le début de la négociation TLS par le client.
  • connectEnd correspond à l'établissement de la connexion au serveur Web.

Mesurer la durée totale de connexion est semblable à la mesure du temps total de résolution DNS: vous soustrayez l'heure de début de l'heure de fin. Toutefois, il existe une propriété secureConnectionStart supplémentaire qui peut être 0 si HTTPS n'est pas utilisé ou si la connexion est persistante. Si vous souhaitez mesurer le temps de négociation TLS, vous devez garder cela à l'esprit:

// Quantifying total connection time
const [pageNav] = performance.getEntriesByType('navigation');
const connectionTime = pageNav.connectEnd - pageNav.connectStart;
let tlsTime = 0; // <-- Assume 0 to start with

// Was there TLS negotiation?
if (pageNav.secureConnectionStart > 0) {
  // Awesome! Calculate it!
  tlsTime = pageNav.connectEnd - pageNav.secureConnectionStart;
}

Une fois la résolution DNS et la négociation de connexion terminées, les délais de récupération des documents et de leurs ressources dépendantes entrent en jeu.

Requêtes et réponses

Les performances de chargement dépendent de deux types de facteurs:

  • Facteurs extrinsiques:latence et bande passante, par exemple. Au-delà du choix d'une société d'hébergement et d'un CDN, nous ne les contrôlons pas (pour la plupart), car les utilisateurs peuvent accéder au Web depuis n'importe où.
  • Facteurs intrinsèques:architectures côté client et serveur, ainsi que taille des ressources et capacité d'optimisation en fonction de ces éléments, qui sont sous notre contrôle.

Ces deux types de facteurs affectent les performances de chargement. Les délais liés à ces facteurs sont essentiels, car ils décrivent le temps nécessaire au téléchargement des ressources. Le temps de navigation et le temps de chargement des ressources décrivent les performances de chargement avec les métriques suivantes:

  • fetchStart marque le moment où le navigateur commence à récupérer une ressource (temps de ressource) ou un document pour une requête de navigation (temps de navigation). Cette étape précède la requête réelle et correspond au moment auquel le navigateur vérifie les caches (par exemple, instances HTTP et Cache).
  • workerStart marque le moment où une requête commence à être traitée dans le gestionnaire d'événements fetch d'un service worker. Cette valeur est 0 si aucun service worker ne contrôle la page actuelle.
  • requestStart correspond au moment où le navigateur effectue la requête.
  • responseStart correspond à l'arrivée du premier octet de la réponse.
  • responseEnd correspond à l'arrivée du dernier octet de la réponse.

Ces délais vous permettent de mesurer plusieurs aspects des performances de chargement, tels que la recherche dans le cache au sein d'un service worker et le temps de téléchargement:

// Cache seek plus response time of the current document
const [pageNav] = performance.getEntriesByType('navigation');
const fetchTime = pageNav.responseEnd - pageNav.fetchStart;

// Service worker time plus response time
let workerTime = 0;

if (pageNav.workerStart > 0) {
  workerTime = pageNav.responseEnd - pageNav.workerStart;
}

Vous pouvez également mesurer d'autres aspects de la latence des requêtes/réponses:

const [pageNav] = performance.getEntriesByType('navigation');

// Request time only (excluding redirects, DNS, and connection/TLS time)
const requestTime = pageNav.responseStart - pageNav.requestStart;

// Response time only (download)
const responseTime = pageNav.responseEnd - pageNav.responseStart;

// Request + response time
const requestResponseTime = pageNav.responseEnd - pageNav.requestStart;

Autres mesures que vous pouvez effectuer

Le timing de navigation et le timing des ressources ne sont pas utiles dans les autres cas que ceux décrits dans les exemples ci-dessus. Vous trouverez ci-dessous d'autres situations pertinentes avec des délais pertinents:

  • Redirections de page:les redirections sont une source négligée de latence accrue, en particulier les chaînes de redirection. La latence est ajoutée de plusieurs façons, telles que des sauts HTTP vers HTTP, ainsi que des redirections 302/301 non mises en cache. Les codes temporels redirectStart, redirectEnd et redirectCount sont utiles pour évaluer la latence de redirection.
  • Déchargement de documents:dans les pages qui exécutent du code dans un gestionnaire d'événements unload, le navigateur doit exécuter ce code avant de pouvoir accéder à la page suivante. unloadEventStart et unloadEventEnd mesurent le déchargement des documents.
  • Traitement des documents:la durée de traitement des documents peut être sans conséquence, sauf si votre site Web envoie des charges utiles HTML très volumineuses. Si cela correspond à votre situation, les dates domInteractive, domContentLoadedEventStart, domContentLoadedEventEnd et domComplete peuvent vous intéresser.

Récupérer des codes temporels dans le code d'application

Tous les exemples présentés jusqu'à présent utilisent performance.getEntriesByType, mais il existe d'autres façons d'interroger le tampon d'entrée des performances, telles que performance.getEntriesByName et performance.getEntries. Ces méthodes sont adaptées lorsque seule une analyse légère est nécessaire. Dans d'autres situations, cependant, ils peuvent introduire un travail excessif dans le thread principal en effectuant une itération sur un grand nombre d'entrées, voire en interrogeant de manière répétée le tampon de performances pour trouver de nouvelles entrées.

L'approche recommandée pour collecter des entrées à partir du tampon d'entrée des performances consiste à utiliser un PerformanceObserver. PerformanceObserver écoute les entrées de performances et les fournit au fur et à mesure qu'elles sont ajoutées au tampon:

// Create the performance observer:
const perfObserver = new PerformanceObserver((observedEntries) => {
  // Get all resource entries collected so far:
  const entries = observedEntries.getEntries();

  // Iterate over entries:
  for (let i = 0; i < entries.length; i++) {
    // Do the work!
  }
});

// Run the observer for Navigation Timing entries:
perfObserver.observe({
  type: 'navigation',
  buffered: true
});

// Run the observer for Resource Timing entries:
perfObserver.observe({
  type: 'resource',
  buffered: true
});

Cette méthode de collecte des codes temporels peut sembler gênante par rapport à l'accès direct au tampon d'entrée des performances. Toutefois, il est préférable d'associer le thread principal à une tâche qui ne remplit pas un rôle essentiel et visible par l'utilisateur.

Accueil Phoning

Une fois que vous avez collecté toutes les durées nécessaires, vous pouvez les envoyer à un point de terminaison pour une analyse plus approfondie. Pour ce faire, vous pouvez utiliser navigator.sendBeacon ou fetch avec l'option keepalive définie. Les deux méthodes envoient une requête à un point de terminaison spécifié de manière non bloquante. La requête est mise en file d'attente de manière à survivre à la session de page actuelle, le cas échéant:

// Caution: If you have lots of performance entries, don't
// do this. This is an example for illustrative purposes.
const data = JSON.stringify(performance.getEntries()));

// The endpoint to transmit the encoded data to
const endpoint = '/analytics';

// Check for fetch keepalive support
if ('keepalive' in Request.prototype) {
  fetch(endpoint, {
    method: 'POST',
    body: data,
    keepalive: true,
    headers: {
      'Content-Type': 'application/json'
    }
  });
} else if ('sendBeacon' in navigator) {
  // Use sendBeacon as a fallback
  navigator.sendBeacon(endpoint, data);
}

Dans cet exemple, la chaîne JSON arrivera dans une charge utile POST que vous pouvez décoder et traiter/stocker dans un backend d'application si nécessaire.

Conclusion

Une fois que vous avez collecté des métriques, c'est à vous de déterminer comment analyser ces données de terrain. Lorsque vous analysez des données de terrain, il existe quelques règles générales à suivre pour vous assurer que vous tirez des conclusions significatives:

  • Évitez les moyennes, car elles ne sont pas représentatives de l'expérience d'un seul utilisateur et peuvent être faussées par des anomalies.
  • Appuyez-vous sur les centiles. Dans les ensembles de données contenant des métriques de performances temporelles, plus il est faible, mieux c'est. En d'autres termes, lorsque vous donnez la priorité aux faibles centiles, vous ne prêtez attention qu'aux expériences les plus rapides.
  • Priorisez la longue traîne de valeurs. Lorsque vous donnez la priorité aux expériences situées au 75e centile ou à un niveau supérieur, vous vous concentrez sur les expériences les plus lentes.

Ce guide n'est pas une ressource exhaustive sur la navigation ou le temps de traitement des ressources, mais constitue un point de départ. Vous trouverez ci-dessous des ressources supplémentaires qui pourraient vous être utiles:

Grâce à ces API et aux données qu'elles fournissent, vous serez mieux à même de comprendre comment les utilisateurs réels affectent les performances de chargement. Vous pourrez ainsi diagnostiquer et résoudre en toute confiance les problèmes de performances de chargement sur le terrain.