Évaluer les performances de chargement sur le terrain avec Navigation Timing et Resource Timing

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

Publié le 8 octobre 2021

Si vous avez utilisé la limitation de connexion dans le panneau "Network" des outils pour les développeurs d'un navigateur (ou Lighthouse dans Chrome) pour évaluer les performances de chargement, vous savez à quel point ces outils sont pratiques pour ajuster les performances. Vous pouvez mesurer rapidement l'impact des optimisations de performances avec 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 fournissent des données expérimentales, et non des données réelles.

Les tests synthétiques ne sont pas mals par nature, 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 vous permettant d'évaluer les performances de chargement sur le terrain

Navigation Timing et Resource Timing sont deux API similaires qui se chevauchent fortement et mesurent deux éléments distincts:

  • Le temps de navigation mesure la vitesse des requêtes de documents HTML (c'est-à-dire les demandes de navigation).
  • Le délai de ressources mesure la vitesse des requêtes pour des ressources dépendantes de documents telles que les fichiers CSS, JavaScript, image et autres.

Ces API présentent leurs données dans un tampon d'entrée des performances, accessible dans le navigateur avec JavaScript. Il existe plusieurs façons de interroger un tampon de performances, mais la méthode 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 d'entrées que vous souhaitez récupérer à partir du tampon d'entrées de performances. 'navigation' et 'resource' récupèrent respectivement les codes temporels pour les API Navigation Timing et Resource Timing.

Ces API peuvent fournir beaucoup d'informations, mais elles sont essentielles pour mesurer les performances de chargement sur le terrain, car vous pouvez recueillir ces données auprès des utilisateurs lorsqu'ils visitent votre site Web.

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

La collecte et l'analyse des délais de navigation et de ressources ressemblent à de l'archéologie, car vous reconstruisez la vie éphémère d'une requête réseau après coup. Il est parfois utile de visualiser des concepts. Pour les requêtes réseau, les outils pour les développeurs de votre navigateur peuvent vous aider.

Délais réseau tels qu'ils apparaissent dans les outils pour les développeurs de Chrome Les durées indiquées concernent la mise en file d'attente des requêtes, la négociation de connexion, la demande elle-même et la réponse affichée dans des barres de couleur.
Visualisation d'une requête réseau dans le panneau réseau des outils pour les développeurs Chrome

La durée 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 et d'autres sources de latence. Ces délais sont représentés par un DOMHighResTimestamp. En fonction de votre navigateur, la précision des codes temporels peut être à la microseconde ou arrondie à la milliseconde. Vous devez examiner ces phases en détail et leur relation avec les paramètres Navigation Timing et Resource Timing.

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 un certain temps, que vous devrez même mesurer sur le terrain. Les paramètres Navigation Timing et Resource Timing exposent deux temps liés au DNS :

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

Vous pouvez calculer la durée totale 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 connexion est un autre facteur déterminant des performances de chargement. Il s'agit de la latence engendrée lors de la connexion à un serveur Web. Si HTTPS est utilisé, ce processus inclura également 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 au moment où la connexion au serveur Web a été établie.

La mesure de la durée totale de la connexion est semblable à la mesure de la durée totale de la résolution DNS : vous soustractez le temps de début du temps 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 à l'esprit les points suivants :

// 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 recherche DNS et la négociation de la connexion terminées, les délais liés à l'extraction des documents et de leurs ressources dépendantes entrent en jeu.

Requêtes et réponses

Les performances de chargement sont affectées par deux types de facteurs :

  • Facteurs externes : latence et bande passante, par exemple. Au-delà du choix d'une société d'hébergement et éventuellement d'un CDN, nous ne sommes (pour la plupart) pas sous notre contrôle, car les utilisateurs peuvent accéder au Web de n'importe où.
  • Facteurs intrinsèques:il s'agit, par exemple, des architectures côté serveur et côté client, ainsi que la taille des ressources et notre capacité à optimiser 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. Navigation Timing et Resource Timing décrivent les performances de chargement avec les métriques suivantes:

  • fetchStart indique que le navigateur commence à extraire une ressource (Resource Timing) ou un document pour une requête de navigation (Navigation Timing). Cette étape précède la requête réelle et correspond au moment où le navigateur vérifie les caches (par exemple, les instances Cache et HTTP).
  • workerStart indique le moment où une requête commence à être gérée dans le gestionnaire d'événements fetch d'un service worker. Il s'agit de 0 lorsqu'aucun service worker ne contrôle la page actuelle.
  • requestStart correspond au moment où le navigateur envoie la requête.
  • responseStart correspond au moment où le premier octet de la réponse arrive.
  • responseEnd correspond au moment où le dernier octet de la réponse arrive.

Ces durées vous permettent de mesurer plusieurs aspects des performances de chargement, tels que la recherche dans le cache dans 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 et des 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 possibles

Les fonctionnalités Navigation Timing et Resource Timing sont utiles dans d'autres cas que les exemples précédents. Voici d'autres situations avec des dates pertinentes qui pourraient nous intéresser:

  • Redirections de page:les redirections sont une source négligée de latence supplémentaire, en particulier les chaînes de redirection. La latence est ajoutée de plusieurs manières, par exemple par des sauts HTTP vers HTTPS, ainsi que par des redirections 302/301 non mises en cache. Les temps redirectStart, redirectEnd et redirectCount sont utiles pour évaluer la latence de redirection.
  • Déchargement de document : sur 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:le temps de traitement des documents n'est pas particulièrement grave, sauf si votre site Web envoie des charges utiles HTML très volumineuses. Si cela correspond à votre situation, les horaires domInteractive, domContentLoadedEventStart, domContentLoadedEventEnd et domComplete peuvent vous intéresser.

Obtenir des temps dans votre code

Tous les exemples présentés jusqu'à présent utilisent performance.getEntriesByType, mais il existe d'autres moyens d'interroger le tampon d'entrée des performances, tels que performance.getEntriesByName et performance.getEntries. Ces méthodes sont adaptées lorsque seule une analyse légère est nécessaire. Toutefois, dans d'autres situations, elles peuvent entraîner un travail excessif du thread principal en effectuant une itération sur un grand nombre d'entrées, ou même 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 à 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, mais il est préférable de relier le thread principal à une tâche qui ne remplit pas un objectif critique ni visible par l'utilisateur.

Appeler chez soi

Une fois que vous avez collecté tous les temps dont vous avez besoin, 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, et la requête est mise en file d'attente de manière à survivre à la session de page en cours si nécessaire:

// Check for navigator.sendBeacon support:
if ('sendBeacon' in navigator) {
  // 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());

  // Send the data!
  navigator.sendBeacon('/analytics', data);
}

Dans cet exemple, la chaîne JSON arrivera dans une charge utile POST que vous pouvez décoder, traiter et 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 champ. Lorsque vous analysez des données de terrain, vous devez suivre quelques règles générales 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 utilisateur donné et peuvent être faussées par des valeurs aberrantes.
  • Utilisez les centiles. Dans les ensembles de données de métriques de performances basées sur le temps, plus la valeur est faible, mieux c'est. Cela signifie que lorsque vous donnez la priorité aux centiles faibles, vous ne prêtez attention qu'aux expériences les plus rapides.
  • Priorisez la longue traîne des valeurs. Lorsque vous donnez la priorité aux expériences correspondant au 75e centile ou plus, vous privilégiez les expériences les plus lentes.

Il ne s'agit pas d'une ressource exhaustive sur la navigation ou la gestion des temps de ressources, mais d'un point de départ. Voici quelques 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 performances de chargement sont perçues par les utilisateurs réels. Vous pourrez ainsi diagnostiquer et résoudre plus facilement les problèmes de performances de chargement sur le terrain.