Tener métricas centradas en el usuario que puedas medir de forma universal en cualquier sitio web es muy valioso. Estas métricas te permiten hacer lo siguiente:
- Comprender cómo los usuarios reales experimentan la Web en su totalidad
- Compara tu sitio con el de la competencia.
- Realiza un seguimiento de datos útiles y prácticos en tus herramientas de estadísticas sin necesidad de escribir código personalizado.
Las métricas universales ofrecen un buen modelo de referencia, pero, en muchos casos, debes medir más que solo estas métricas para captar la experiencia completa de tu sitio en particular.
Las métricas personalizadas te permiten medir aspectos de la experiencia de tu sitio que solo pueden aplicarse a él, como los siguientes:
- Es el tiempo que tarda una app de una sola página (SPA) en realizar la transición de una "página" a otra.
- Es el tiempo que tarda una página en mostrar datos recuperados de una base de datos para los usuarios que accedieron.
- Cuánto tarda una app renderizada del servidor (SSR) en hidratarse.
- Es la tasa de aciertos de caché para los recursos cargados por los visitantes recurrentes.
- La latencia del evento 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 hacks 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 la delta entre cada fotograma. Si la diferencia es mucho mayor que la velocidad de fotogramas de la pantalla, puedes informarla como una tarea larga. Sin embargo, no se recomiendan estos hacks, ya que en realidad afectan el rendimiento (por ejemplo, agotan la batería).
La primera regla de una medición de rendimiento eficaz es asegurarse de que tus técnicas de medición de rendimiento no causen problemas de rendimiento. Por lo tanto, para las métricas personalizadas que mides en tu 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. Comprenderlo es fundamental para obtener buenos datos.
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 inactivos, lo que significa que, por lo general, no interferirán en 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 nuevas entradas de rendimiento. Luego, le indicas al observador qué tipos de entradas 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 la observación, 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 las entradas a medida que ocurren. Esto puede causar problemas si deseas cargar de forma diferida tu código de estadísticas de rendimiento para que no bloquee los recursos de prioridad más alta.
Para obtener entradas históricas (después de que se hayan producido), 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 tu devolución de llamada de PerformanceObserver
, hasta el tamaño máximo del búfer para ese tipo.
po.observe({
type: 'some-entry-type',
buffered: true,
});
APIs de rendimiento heredadas que debes 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
, sino solo a través de PerformanceObserver
.
A menos que necesites específicamente la compatibilidad con Internet Explorer, es mejor 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 API de medición de uso general para métricas basadas en el tiempo. Te permite marcar puntos de forma arbitraria en el tiempo y, luego, medir la duración entre esas marcas más adelante.
// 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, Chrome DevTools visualiza las mediciones de tiempos del usuario en el panel Rendimiento, y muchos proveedores de estadísticas también harán un seguimiento automático de las mediciones que realices y enviarán los datos de duración a su backend de estadísticas.
Para informar las mediciones de la sincronización del usuario, puedes usar PerformanceObserver y registrarte para observar las 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 está bloqueado 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 compilan en la propia API de Long Tasks (como Time to Interactive (TTI) y Total Blocking Time (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 los fotogramas largos, puedes usar PerformanceObserver y registrarte para observar las 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. Sin embargo, 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 compila sobre la API de Element Timing y agrega informes automáticos del elemento con contenido más grande, pero también puedes generar informes 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 Interaction to Next Paint (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 una página. Por lo general, la INP de una página es la interacción que más tardó en completarse, desde el momento en que el usuario inició la interacción hasta el momento en que el navegador pinta el siguiente fotograma que muestra el resultado visual de la entrada del usuario.
La métrica de INP es posible 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 en que el navegador recibe el evento.processingStart
: Es el momento en que el navegador puede comenzar a procesar los controladores de eventos para el evento.processingEnd
: Es la hora en la que el navegador termina de ejecutar todo el código síncrono iniciado desde los controladores de eventos para este evento.duration
: Es el tiempo (redondeado a 8 milisegundos por motivos de seguridad) entre el momento en que el navegador recibe el evento y el momento en que puede pintar el siguiente fotograma después de terminar de ejecutar todo el código síncrono iniciado desde los controladores de eventos.
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 les brinda a los desarrolladores estadísticas detalladas 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 se limita solo a los datos de tiempo (aunque 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]: 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 recursos, este valor puede ser mucho menor queencodedBodySize
y, en algunos casos, puede ser cero (si no se requiere una nueva validación de la 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.
En el siguiente ejemplo, se registran todos los recursos que solicitó la página y se 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 API de Resource Timing, pero solo informa las solicitudes de navegación. El tipo de entrada navigation
también es similar al tipo de entrada resource
, pero contiene cierta información adicional específica solo para las solicitudes de navegación (como cuando se activan los eventos DOMContentLoaded
y load
).
Una métrica de la que muchos desarrolladores realizan un seguimiento para comprender el tiempo de respuesta del servidor (tiempo hasta el primer byte (TTFB)) está disponible con 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 interesar a los desarrolladores que usan service workers es el tiempo de inicio del service worker para las solicitudes de navegación. Esta es la cantidad de tiempo que tarda el navegador en iniciar el subproceso del trabajador de servicio antes de que pueda comenzar a 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 tiempo específicos de la solicitud de tu servidor al navegador a través de encabezados de respuesta. Por ejemplo, puedes indicar cuánto tiempo tardó en buscar datos en una base de datos para una solicitud en particular, lo que puede ser útil para depurar problemas de rendimiento causados por la lentitud del servidor.
Para los desarrolladores que usan proveedores de estadísticas de terceros, la API de Server Timing es la única forma de correlacionar los datos de rendimiento del servidor con otras métricas empresariales que pueden medir estas herramientas de estadísticas.
Para especificar los datos de tiempo 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});