Es muy valioso contar con métricas centradas en los usuarios que se puedan medir de forma universal en un sitio web determinado. Estas métricas te permiten hacer lo siguiente:
- Comprende cómo los usuarios reales experimentan la Web como un todo.
- Compara tu sitio con el de la competencia.
- Realiza un seguimiento de los datos útiles y prácticos en tus herramientas de análisis sin necesidad de escribir código personalizado.
Las métricas universales ofrecen un buen modelo de referencia; sin embargo, en muchos casos, debes medir más que solo estas métricas para capturar la experiencia completa de tu sitio en particular.
Las métricas personalizadas te permiten medir aspectos de la experiencia de tu sitio que solo podrían aplicarse a él, como los siguientes:
- Cuánto tiempo tarda una aplicación de una sola página (SPA) en pasar de una "página" a otro.
- Cuánto tarda una página en mostrar los datos recuperados de una base de datos para los usuarios que accedieron.
- Cuánto tarda una app renderizada del servidor (SSR) en hidratarse.
- La tasa de aciertos de caché para los recursos cargados por los visitantes recurrentes.
- Es la latencia de los eventos de clic o de teclado en un juego.
APIs para medir métricas personalizadas
Históricamente, los desarrolladores web no tenían muchas APIs de bajo nivel para medir el rendimiento y, como resultado, tuvieron que recurrir a hackeos para medir si un sitio tenía un buen rendimiento.
Por ejemplo, es posible determinar si el subproceso principal está bloqueado debido a tareas de JavaScript de larga duración ejecutando un bucle requestAnimationFrame
y calculando el delta entre cada marco. Si el delta es significativamente más largo que la velocidad de fotogramas de la pantalla, puedes informarlo como una tarea larga. Sin embargo, no se recomiendan esos trucos porque, en realidad, afectan el rendimiento por sí mismos (por ejemplo, agotando la batería).
La primera regla de una medición eficaz del rendimiento es asegurarse de que las técnicas de medición del rendimiento no causen problemas de rendimiento. Por lo tanto, para cualquier métrica personalizada que mida en su sitio, es mejor usar una de las siguientes APIs, si es posible.
API de Performance Observer
La API de Performance Observer es el mecanismo que recopila y muestra datos de todas las demás APIs de rendimiento que se analizan en esta página. Es fundamental comprenderlos para obtener datos correctos.
Puedes usar PerformanceObserver
para suscribirte de forma pasiva a eventos relacionados con el rendimiento. Esto permite que las devoluciones de llamada de la API se activen durante los períodos de inactividad, lo que significa que, por lo general, no interfieren con el rendimiento de la página.
Para crear un PerformanceObserver
, pásale una devolución de llamada para que se ejecute cada vez que se envíen entradas de rendimiento nuevas. Luego, le dices al observador qué tipos de entradas debe escuchar con el método observe()
:
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
po.observe({type: 'some-entry-type'});
En las siguientes secciones, se enumeran todos los tipos de entradas disponibles para observar, pero en los navegadores más nuevos, puedes inspeccionar qué tipos de entradas están disponibles a través de la propiedad estática PerformanceObserver.supportedEntryTypes
.
Observa las entradas que ya ocurrieron
De forma predeterminada, los objetos PerformanceObserver
solo pueden observar entradas a medida que ocurren. Esto puede causar problemas si deseas realizar una carga diferida de tu código de análisis de rendimiento para que no bloquee los recursos de mayor prioridad.
Para obtener entradas históricas (después de que ocurrieron), establece la marca buffered
en true
cuando llames a observe()
. El navegador incluirá entradas históricas de su búfer de entrada de rendimiento la primera vez que se llame a la devolución de llamada PerformanceObserver
, hasta el tamaño de búfer máximo para ese tipo.
po.observe({
type: 'some-entry-type',
buffered: true,
});
APIs de rendimiento heredadas que se deben evitar
Antes de la API de Performance Observer, los desarrolladores podían acceder a las entradas de rendimiento con los siguientes tres métodos definidos en el objeto performance
:
Si bien estas APIs aún son compatibles, no se recomienda su uso porque no te permiten escuchar cuando se emiten entradas nuevas. Además, muchas APIs nuevas (como largest-contentful-paint
) no se exponen a través del objeto performance
, solo se exponen a través de PerformanceObserver
.
A menos que necesites compatibilidad específicamente con Internet Explorer, se recomienda evitar estos métodos en tu código y usar PerformanceObserver
en el futuro.
API de User Timing
La API de User Timing es una herramienta de uso general de medición para las métricas basadas en el tiempo. Te permite marcar de forma arbitraria puntos en y luego medirás la duración entre esas marcas.
// Record the time immediately before running a task.
performance.mark('myTask:start');
await doMyTask();
// Record the time immediately after running a task.
performance.mark('myTask:end');
// Measure the delta between the start and end of the task
performance.measure('myTask', 'myTask:start', 'myTask:end');
Si bien las APIs como Date.now()
o performance.now()
te brindan capacidades similares, el beneficio de usar la API de User Timing es que se integra bien con las herramientas de rendimiento. Por ejemplo, las Herramientas para desarrolladores de Chrome visualizan las mediciones de Tiempos de usuario en el panel Rendimiento, y muchos proveedores de herramientas de análisis también realizan un seguimiento automático de cualquier medición que realices y envían los datos de duración a su backend de análisis.
Para generar informes sobre las mediciones de User Timing, puedes usar PerformanceObserver y registrarte para observar entradas de tipo measure
:
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `measure` entries to be dispatched.
po.observe({type: 'measure', buffered: true});
API de Long Tasks
La API de Long Tasks es útil para saber cuándo el subproceso principal del navegador se bloquea durante el tiempo suficiente como para afectar la velocidad de fotogramas o la latencia de entrada. La API informará sobre cualquier tarea que se ejecute durante más de 50 milisegundos.
Cada vez que necesites ejecutar un código costoso, o cargar y ejecutar secuencias de comandos grandes, es útil realizar un seguimiento si ese código bloquea el subproceso principal. De hecho, muchas métricas de nivel superior se basan en la API de Long Tasks (como el tiempo de carga (TTI) y el tiempo de bloqueo total (TBT)).
Para determinar cuándo ocurren tareas largas, puedes usar PerformanceObserver y registrarte para observar entradas de tipo longtask
:
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `longtask` entries to be dispatched.
po.observe({type: 'longtask', buffered: true});
API de Long Animation Frames
La API de Long Animation Frames es una nueva iteración de la API de Long Tasks que analiza fotogramas largos, en lugar de tareas largas, de más de 50 milisegundos. Esto aborda algunas deficiencias de la API de Long Tasks, incluida una mejor atribución y el alcance más amplio de posibles retrasos problemáticos.
Para determinar cuándo ocurren fotogramas largos, puedes usar PerformanceObserver y registrarte para observar entradas de tipo long-animation-frame
:
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `long-animation-frame` entries to be dispatched.
po.observe({type: 'long-animation-frame', buffered: true});
API de Element Timing
La métrica Procesamiento de imagen con contenido más grande (LCP) es útil para saber cuándo se pintó la imagen o el bloque de texto más grande en la pantalla, pero, en algunos casos, deseas medir el tiempo de renderización de un elemento diferente.
En estos casos, usa la API de Element Timing. En realidad, la API de LCP se basa en la API de Element Timing y agrega informes automáticos del elemento con contenido más grande, pero también puedes informar sobre otros elementos si les agregas explícitamente el atributo elementtiming
y registras un PerformanceObserver para observar el tipo de entrada element
.
<img elementtiming="hero-image" />
<p elementtiming="important-paragraph">This is text I care about.</p>
<!-- ... -->
<script>
const po = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `element` entries to be dispatched.
po.observe({type: 'element', buffered: true});
</script>
API de Event Timing
La métrica Interacción a la siguiente pintura (INP) evalúa la capacidad de respuesta general de la página observando todas las interacciones de clic, presión y teclado durante el ciclo de vida de la página. El INP de una página suele ser la interacción que más tiempo tardó en completarse, desde el momento en que el usuario inició la interacción hasta el momento en que el navegador pinta el siguiente cuadro que muestra el resultado visual de la entrada del usuario.
La métrica INP funciona gracias a la API de Event Timing. Esta API expone varias marcas de tiempo que ocurren durante el ciclo de vida del evento, incluidas las siguientes:
startTime
: Es la hora a la que el navegador recibe el evento.processingStart
: Es la hora a la que el navegador puede comenzar a procesar los controladores del evento.processingEnd
: Es la hora a la que el navegador termina de ejecutar todo el código síncrono iniciado desde los controladores del evento para este evento.duration
: Es el tiempo (redondeado a 8 milisegundos por motivos de seguridad) que transcurre desde que el navegador recibe el evento hasta que puede pintar el siguiente fotograma después de terminar de ejecutar todo el código síncrono que se inició desde los controladores del evento.
En el siguiente ejemplo, se muestra cómo usar estos valores para crear mediciones personalizadas:
const po = new PerformanceObserver((entryList) => {
// Get the last interaction observed:
const entries = Array.from(entryList.getEntries()).forEach((entry) => {
// Get various bits of interaction data:
const inputDelay = entry.processingStart - entry.startTime;
const processingTime = entry.processingEnd - entry.processingStart;
const presentationDelay = entry.startTime + entry.duration - entry.processingEnd;
const duration = entry.duration;
const eventType = entry.name;
const target = entry.target || "(not set)"
console.log("----- INTERACTION -----");
console.log(`Input delay (ms): ${inputDelay}`);
console.log(`Event handler processing time (ms): ${processingTime}`);
console.log(`Presentation delay (ms): ${presentationDelay}`);
console.log(`Total event duration (ms): ${duration}`);
console.log(`Event type: ${eventType}`);
console.log(target);
});
});
// A durationThreshold of 16ms is necessary to include more
// interactions, since the default is 104ms. The minimum
// durationThreshold is 16ms.
po.observe({type: 'event', buffered: true, durationThreshold: 16});
API de Resource Timing
La API de Resource Timing ofrece a los desarrolladores información detallada sobre cómo se cargaron los recursos de una página en particular. A pesar del nombre de la API, la información que proporciona no solo se limita a los datos de tiempo (aunque los hay muchos). Entre otros datos a los que puedes acceder, se incluyen los siguientes:
initiatorType
: Indica cómo se recuperó el recurso, por ejemplo, desde una etiqueta<script>
o<link>
, o desde una llamadafetch()
.nextHopProtocol
: Es el protocolo que se usa para recuperar el recurso, comoh2
oquic
.encodedBodySize
/decodedBodySize]: el tamaño del recurso en su forma codificado o decodificado (respectivamente).transferSize
: Es el tamaño del recurso que se transfirió a través de la red. Cuando la caché entrega los recursos, este valor puede ser mucho menor queencodedBodySize
y, en algunos casos, puede ser cero (si no se requiere revalidación de caché).
Puedes usar la propiedad transferSize
de las entradas de tiempo de recursos para medir una métrica de tasa de aciertos de caché o una métrica de tamaño total de recursos almacenados en caché, que puede ser útil para comprender cómo tu estrategia de almacenamiento en caché de recursos afecta el rendimiento de los visitantes recurrentes.
El siguiente ejemplo registra todos los recursos solicitados por la página e indica si la caché entregó o no cada recurso.
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled using the cache.
console.log(entry.name, entry.transferSize === 0);
}
});
// Start listening for `resource` entries to be dispatched.
po.observe({type: 'resource', buffered: true});
API de Navigation Timing
La API de Navigation Timing es similar a la de Resource Timing, pero solo informa solicitudes de navegación. El tipo de entrada navigation
también es similar al tipo de entrada resource
, pero contiene información adicional específica de solo las solicitudes de navegación (como cuándo se activan los eventos DOMContentLoaded
y load
).
Una métrica a la que muchos desarrolladores hacen un seguimiento para comprender el tiempo de respuesta del servidor (Tiempo hasta el primer byte (TTFB)) está disponible mediante la API de Navigation Timing, específicamente, la marca de tiempo responseStart
de la entrada.
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled using the cache.
console.log('Time to first byte', entry.responseStart);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});
Otra métrica que puede interesarles a los desarrolladores que usan service worker es el tiempo de inicio del service worker para solicitudes de navegación. Esta es la cantidad de tiempo que tarda el navegador en iniciar el subproceso del service worker antes de poder interceptar eventos de recuperación.
El tiempo de inicio del service worker para una solicitud de navegación en particular se puede determinar a partir del delta entre entry.responseStart
y entry.workerStart
.
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Service Worker startup time:',
entry.responseStart - entry.workerStart);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});
API de Server Timing
La API de Server Timing te permite pasar datos de sincronización específicos de la solicitud del servidor al navegador a través de encabezados de respuesta. Por ejemplo, puedes indicar cuánto tiempo llevó buscar datos en una base de datos para una solicitud en particular, lo que puede ser útil en la depuración de problemas de rendimiento causados por la lentitud en el servidor.
Para los desarrolladores que usan proveedores de estadísticas externos, la API de Server Timing es la única forma de correlacionar los datos de rendimiento del servidor con otras métricas empresariales que estas herramientas de análisis pueden estar midiendo.
Para especificar los datos de sincronización del servidor en tus respuestas, puedes usar el encabezado de respuesta Server-Timing
. A continuación, se muestra un ejemplo.
HTTP/1.1 200 OK
Server-Timing: miss, db;dur=53, app;dur=47.2
Luego, desde tus páginas, puedes leer estos datos en las entradas resource
o navigation
de las APIs de Resource Timing y Navigation Timing.
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Logs all server timing data for this response
console.log('Server Timing', entry.serverTiming);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});