Cómo evaluar el rendimiento de carga en el campo con Navigation Timing y Resource Timing

Aprende los conceptos básicos del uso de las APIs de Navigation y Resource Timing para evaluar el rendimiento de carga en el campo.

Si usaste la limitación de conexión en el panel de red de las herramientas para desarrolladores de un navegador (o Lighthouse en Chrome) para evaluar el rendimiento de carga, sabrás lo convenientes que son esas herramientas para ajustar el rendimiento. Puedes medir rápidamente el impacto de las optimizaciones de rendimiento con una velocidad de conexión de referencia constante y estable. El único problema es que se trata de las pruebas sintéticas, que generan datos de lab, no datos de campo.

Las pruebas sintéticas no son intrínsecamente malas, pero no representan la velocidad de carga de tu sitio web para los usuarios reales. Esto requiere datos de campo, que puedes recopilar de las APIs de Navigation Timing y Resource Timing.

APIs para ayudarte a evaluar el rendimiento de carga en el campo

Navigation Timing y Resource Timing son dos APIs similares con una superposición significativa que miden dos aspectos distintos:

  • Navigation Timing mide la velocidad de las solicitudes de documentos HTML (es decir, solicitudes de navegación).
  • Resource Timing mide la velocidad de las solicitudes de recursos que dependen de documentos, como CSS, JavaScript, imágenes, etcétera.

Estas APIs exponen sus datos en un búfer de entrada de rendimiento, al que se puede acceder desde el navegador con JavaScript. Existen varias formas de consultar un búfer de rendimiento, pero una forma común es usar performance.getEntriesByType:

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

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

performance.getEntriesByType acepta una cadena que describe el tipo de entradas que deseas recuperar del búfer de entradas de rendimiento. 'navigation' y 'resource' recuperan los tiempos de las APIs de Navigation Timing y Resource Timing, respectivamente.

La cantidad de información que proporcionan estas APIs puede ser abrumadora, pero son la clave para medir el rendimiento de carga en el campo, ya que puedes recopilar estos tiempos de los usuarios mientras visitan tu sitio web.

La vida y los tiempos de una solicitud de red

Recopilar y analizar tiempos de navegación y recursos es como la arqueología en el sentido de que se reconstruye la vida fugaz de una solicitud de red después del hecho. A veces, es útil visualizar conceptos, y en lo que respecta a las solicitudes de red, las herramientas para desarrolladores de tu navegador pueden ayudarte.

Diagrama de los tiempos de red como se muestra en las Herramientas para desarrolladores de Chrome. Los tiempos que se muestran son para la cola de solicitudes, la negociación de conexión, la solicitud en sí y la respuesta en barras con código de color.
Una visualización de una solicitud de red en el panel de red de las Herramientas para desarrolladores de Chrome

La vida de una solicitud de red tiene fases distintas, como búsqueda de DNS, establecimiento de conexión, negociación de TLS, etc. Estos tiempos se representan como DOMHighResTimestamp. Según el navegador, el nivel de detalle de los tiempos puede reducirse a microsegundos o redondearse a milisegundos. Examinemos estas fases en detalle y cómo se relacionan con Navigation Timing y Resource Timing.

búsqueda de DNS

Cuando un usuario visita una URL, se consulta el sistema de nombres de dominio (DNS) para traducir un dominio a una dirección IP. Este proceso puede demorar bastante tiempo; incluso puedes medir tiempo en el campo. Navigation Timing y Resource Timing exponen dos tiempos relacionados con DNS:

  • domainLookupStart es cuando comienza la búsqueda de DNS.
  • domainLookupEnd es cuando finaliza la búsqueda de DNS.

Para calcular el tiempo total de búsqueda de DNS, se debe restar la métrica inicial de la métrica final:

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

Negociación de conexión

Otro factor que contribuye al rendimiento de carga es la negociación de conexión, que es la latencia que se genera cuando se conecta a un servidor web. Si se usa HTTPS, este proceso también incluirá el tiempo de negociación de TLS. La fase de conexión consta de tres tiempos:

  • connectStart ocurre cuando el navegador comienza a abrir una conexión con un servidor web.
  • secureConnectionStart marca cuando el cliente comienza la negociación TLS.
  • connectEnd es cuando se establece la conexión con el servidor web.

Medir el tiempo total de conexión es similar a medir el tiempo total de búsqueda de DNS: se resta el tiempo de inicio al de finalización. Sin embargo, hay una propiedad secureConnectionStart adicional que puede ser 0 si no se usa HTTPS o si la conexión es persistente. Si quieres medir el tiempo de negociación TLS, debes tenerlo en cuenta:

// 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;
}

Una vez finalizada la búsqueda de DNS y la negociación de conexiones, entran en juego los tiempos relacionados con la recuperación de documentos y los recursos dependientes.

Solicitudes y respuestas

El rendimiento de carga se ve afectado por dos tipos de factores:

  • Factores extrínsecos: Estos son aspectos como la latencia y el ancho de banda. Más allá de elegir una empresa de hosting y una CDN, están (casi) fuera de nuestro control, ya que los usuarios pueden acceder a la Web desde cualquier lugar.
  • Factores intrínsecos: Son aspectos como las arquitecturas del servidor y del cliente, así como el tamaño de los recursos y nuestra capacidad de optimizar esos aspectos que están bajo nuestro control.

Ambos tipos de factores afectan el rendimiento de carga. Los tiempos relacionados con estos factores son fundamentales, ya que describen cuánto tardan los recursos en descargarse. Tanto Navigation Timing como Resource Timing describen el rendimiento de carga con las siguientes métricas:

  • fetchStart marca el momento en que el navegador comienza a recuperar un recurso (Resource Timing) o un documento para una solicitud de navegación (Navigation Timing). Esto precede a la solicitud real y es el punto en el que el navegador verifica las memorias caché (por ejemplo, HTTP y instancias Cache).
  • workerStart marca cuándo se comienza a controlar una solicitud en el controlador de eventos fetch de un service worker. Este valor será 0 cuando ningún service worker controle la página actual.
  • requestStart es cuando el navegador realiza la solicitud.
  • responseStart es cuando llega el primer byte de la respuesta.
  • responseEnd es cuando llega el último byte de la respuesta.

Estos tiempos te permiten medir varios aspectos del rendimiento de carga, como la búsqueda de caché dentro de un service worker y el tiempo de descarga:

// 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;
}

También puedes medir otros aspectos de la latencia de solicitud o respuesta:

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;

Otras medidas que puedes tomar

La sincronización de navegación y de recursos es útil para mucho más de lo que se describe en los ejemplos anteriores. Estas son algunas otras situaciones con tiempos relevantes que podría valer la pena explorar:

  • Redireccionamientos de página:Son una fuente de latencia adicional que se pasa por alto, en especial las cadenas de redireccionamiento. La latencia se agrega de varias maneras, como en los saltos de HTTP a HTTP y en los redireccionamientos 302/no almacenados en caché. Los tiempos de redirectStart, redirectEnd y redirectCount son útiles para evaluar la latencia de redireccionamiento.
  • Descarga de documentos: En las páginas que ejecutan código en un controlador de eventos unload, el navegador debe ejecutar ese código para poder navegar a la página siguiente. unloadEventStart y unloadEventEnd miden la descarga de documentos.
  • Procesamiento de documentos: Es posible que el tiempo de procesamiento de documentos no sea consecuencia, a menos que tu sitio web envíe cargas útiles HTML muy grandes. Si esto describe tu situación, los tiempos de domInteractive, domContentLoadedEventStart, domContentLoadedEventEnd y domComplete pueden ser de interés.

Adquiere tiempos en el código de la aplicación

Todos los ejemplos que se muestran hasta ahora usan performance.getEntriesByType, pero hay otras formas de consultar el búfer de entrada de rendimiento, como performance.getEntriesByName y performance.getEntries. Estos métodos son adecuados cuando solo se necesita un análisis de la luz. En otras situaciones, sin embargo, pueden introducir un exceso de trabajo en el subproceso principal a través de la iteración sobre una gran cantidad de entradas o incluso el sondeo del búfer de rendimiento de forma repetida para encontrar nuevas entradas.

El enfoque recomendado para recopilar entradas del búfer de entrada de rendimiento es usar un PerformanceObserver. PerformanceObserver escucha las entradas de rendimiento y las proporciona a medida que se agregan al búfer:

// 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
});

Este método de recopilación de tiempos puede resultar incómodo cuando se compara con el acceso directo al búfer de entrada de rendimiento, pero es preferible vincular el subproceso principal con trabajo que no tiene un propósito crítico para el usuario.

Llamando a casa

Una vez que haya recopilado todos los tiempos que necesita, puede enviarlos a un endpoint para su análisis más detallado. Dos maneras de hacerlo son con navigator.sendBeacon o con un fetch con la opción keepalive establecida. Ambos métodos enviarán una solicitud a un extremo específico sin bloqueos y la solicitud se pondrá en cola de forma tal que sobreviva a la sesión de la página actual en caso de ser necesario:

// 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);
}

En este ejemplo, la string JSON llegará a una carga útil POST que puedes decodificar y procesar/almacenar en el backend de una aplicación según sea necesario.

Conclusión

Una vez que recopilaste las métricas, depende de ti averiguar cómo analizar los datos de esos campos. Al analizar datos de campo, hay algunas reglas generales que debes seguir para asegurarte de sacar conclusiones significativas:

  • Evita los promedios, ya que no representan la experiencia de ningún usuario y pueden estar sesgados por valores atípicos.
  • Confía en los percentiles. En conjuntos de datos de métricas de rendimiento basadas en el tiempo, menor es mejor. Esto significa que, cuando priorizas percentiles bajos, solo prestas atención a las experiencias más rápidas.
  • Prioriza la cola larga de valores. Cuando priorizas las experiencias que se encuentran en el percentil 75 o superior, te enfocas en lo que corresponde: en las experiencias más lentas.

Esta guía no pretende ser un recurso exhaustivo sobre Navigation o Resource Timing, sino un punto de partida. Estos son algunos recursos adicionales que podrían resultarte útiles:

Con estas APIs y los datos que proporcionan, estarás mejor equipado para comprender cómo los usuarios reales experimentan el rendimiento de carga, lo que te dará más confianza a la hora de diagnosticar y abordar problemas de rendimiento de carga en el campo.