Métricas personalizadas
Es muy valioso tener métricas centradas en el usuario que puedas medir, universalmente, en cualquier sitio web. Estas métricas te permiten:
- Comprender cómo los usuarios reales experimentan la web en su totalidad
- Comparar fácilmente tu sitio con la competencia
- Realizar un seguimiento de datos útiles y procesables en tus herramientas de análisis sin necesidad de escribir código personalizado
Las métricas universales ofrecen una buena línea de base, pero en muchos casos es necesario medir más que estas métricas para capturar la experiencia completa de su sitio en particular.
Las métricas personalizadas te permiten medir aspectos de la experiencia de tu sitio que solo pueden aplicarse a tu sitio, como lo pueden ser:
- Cuánto tiempo tarda una aplicación de una sola página (SPA) en pasar de una "página" a otra
- Cuánto tiempo tarda una página en mostrar los datos obtenidos de una base de datos para los usuarios que han iniciado sesión
- Cuánto tiempo tarda una aplicación renderizada en el lado del servidor (SSR) en hydrate
- La tasa de aciertos de caché para los recursos cargados por visitantes recurrentes
- La latencia de eventos de un clic o de un teclado en un juego
API para medir métricas personalizadas #
Históricamente, los desarrolladores web no han tenido muchas API de bajo nivel para medir el rendimiento y como resultado han tenido que recurrir a hacks para medir si un sitio estaba funcionando bien.
Por ejemplo, es posible determinar si el hilo principal está bloqueado debido a tareas de JavaScript de larga duración mediante el uso de requestAnimationFrame
y calculando el delta entre cada fotograma. Si el delta es significativamente más largo que la velocidad de fotogramas de la pantalla, puedes determinarlo como una tarea larga. Sin embargo, estos trucos no se recomiendan porque en realidad ellos mismos afectan el rendimiento (por ejemplo, drenando la batería del dispositivo).
La primera regla de la medición del desempeño efectiva es asegurarse de que tus técnicas de medición del desempeño no causen problemas de desempeño. Por lo tanto, para cualquier métrica personalizada que midas en tu sitio, es mejor usar una de las siguientes API si es posible.
Performance Observer (Observador de desempeño) #
Comprender la API de PerformanceObserver es fundamental para crear métricas de rendimiento personalizadas porque es el mecanismo mediante el cual obtiene datos de todas las demás API de rendimiento que se analizan en este artículo.
Con PerformanceObserver
, podrás suscribirse pasivamente a eventos relacionados con el rendimiento, lo que significa que estas API generalmente no interferirán con el rendimiento de la página, ya que sus retrollamadas (callbacks) generalmente se activan durante los períodos de inactividad.
Puedes crear un PerformanceObserver
pasándole una retrollamada para que se ejecute siempre que se envíen nuevas entradas de rendimiento. Luego le dice al observador qué tipos de entradas debe escuchar a través del método observe()
:
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
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'});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Las siguientes secciones enumeran todos los tipos de entradas disponibles para la observación, pero en los navegadores más recientes puedes inspeccionar qué tipos de entrada están disponibles a través de la propiedad PerformanceObserver.supportedEntryTypes
.
Observando entradas que ya sucedieron #
De forma predeterminada, los objetos de PerformanceObserver
solo pueden observar las entradas a medida que ocurren. Esto puede ser problemático si deseas cargar tu código de análisis de rendimiento de forma diferida (para no bloquear los recursos de mayor prioridad).
Para obtener entradas históricas (después de que hayan ocurrido), define la bandera de buffered
a 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 retrollamada de PerformanceObserver
.
po.observe({
type: 'some-entry-type',
buffered: true,
});
API de rendimiento obsoletas que se deben de evitar #
Antes de la API de Performance Observer, los desarrolladores podían acceder a las entradas de rendimiento mediante los siguientes tres métodos definidos en el objeto de performance
:
Si bien estas API aún son compatibles, no se recomienda su uso porque no te permiten escuchar cuándo se emiten nuevas entradas. Además, muchas API nuevas (como Long Tasks) no se exponen a través del performance
, solo se exponen a través del PerformanceObserver
.
A menos que necesites específicamente compatibilidad con Internet Explorer, es mejor evitar estos métodos en su código y usar PerformanceObserver
en el futuro.
API de User Timing #
La API de User Timing (Tiempo de usuario) es un API de medición de propósito general para métricas basadas en el tiempo. Te permite marcar arbitrariamente puntos en el tiempo y luego medir 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 API como Date.now()
o performance.now()
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 User Timing en el panel de Rendimiento, y muchos proveedores de análisis también realizarán un seguimiento automático de cualquier medición que se realice y enviarán los datos de duración a su back-end de análisis.
Para informar las mediciones del User Timing puede utilizar PerformanceObserver y registrarlas para observar las entradas de tipo measure
:
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
API de Long Tasks #
La API de Long Tasks (Tareas largas) es útil para saber cuándo el hilo principal del navegador está bloqueado durante el tiempo suficiente para afectar la velocidad de fotogramas o la latencia de entrada. Actualmente, la API informará sobre cualquier tarea que se ejecute durante más de 50 milisegundos (ms).
Siempre que necesites ejecutar un código costoso (o cargar y ejecutar scripts de gran tamaño), es útil realizar un seguimiento de si ese código bloqueó o no el hilo principal. De hecho, muchas métricas de nivel superior se construyen sobre la API de Long Tasks (como Time to Interactive (TTI): Tiempo para interactividad y Total Blocking Time (TBT): Tiempo total de bloqueo).
Para determinar cuándo ocurren las tareas largas, puedes usar PerformanceObserver y registrarlas para observar entradas de tipo longtask
:
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
API de Element Timing #
La métrica de Largest Contentful Paint (LCP): Despliegue del contenido más extenso 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 se desea medir el tiempo de renderizado de un elemento diferente.
Para estos casos, puedes utilizar la API Element Timing (Tiempo de elementos). De hecho, la API de Largest Contentful Paint en realidad se basa en la API de Element Timing y agrega informes automáticos del elemento con contenido más grande, pero puede informar sobre elementos adicionales agregando explícitamente el elementtiming
y registrando un PerformanceObserver para su observación del tipo de entrada del elemento.
<img elementtiming="hero-image" />
<p elementtiming="important-paragraph">This is text I care about.</p>
...
<script>
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
</script>
API de Event Timing #
La métrica First Input Delay (FID): Demora de la primera entrada mide el tiempo desde que un usuario interactúa por primera vez con una página hasta el momento en que el navegador puede comenzar a procesar los controladores de eventos en respuesta a esa interacción. Sin embargo, en algunos casos también puede ser útil medir el tiempo de procesamiento del evento en sí, así como el tiempo hasta que se pueda procesar el siguiente fotograma.
Esto es posible con la API de Event Timing (Tiempo del evento) (que se utiliza para medir el FID), ya que expone una serie de marcas de tiempo en el ciclo de vida del evento, que incluyen:
startTime
: el tiempo cuando el navegador recibe el evento.processingStart
: el tiempo en que el navegador puede comenzar a procesar los controladores de eventos para el evento.processingEnd
: el tiempo en que el navegador termina de ejecutar todo el código síncrono iniciado desde los controladores de eventos para este evento.duration
: el tiempo (redondeado a 8 ms por razones de seguridad) entre el momento en que el navegador recibe el evento hasta que puede pintar el siguiente cuadro después de terminar de ejecutar todo el código síncrono iniciado desde los controladores de eventos.
El siguiente ejemplo muestra cómo utilizar estos valores para crear medidas personalizadas:
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
const po = new PerformanceObserver((entryList) => {
const firstInput = entryList.getEntries()[0];
// Measure First Input Delay (FID).
const firstInputDelay = firstInput.processingStart - firstInput.startTime;
// Measure the time it takes to run all event handlers
// Note: this does not include work scheduled asynchronously using
// methods like `requestAnimationFrame()` or `setTimeout()`.
const firstInputProcessingTime = firstInput.processingEnd - firstInput.processingStart;
// Measure the entire duration of the event, from when input is received by
// the browser until the next frame can be painted after processing all
// event handlers.
// Note: similar to above, this value does not include work scheduled
// asynchronously using `requestAnimationFrame()` or `setTimeout()`.
// And for security reasons, this value is rounded to the nearest 8ms.
const firstInputDuration = firstInput.duration;
// Log these values the console.
console.log({
firstInputDelay,
firstInputProcessingTime,
firstInputDuration,
});
});
po.observe({type: 'first-input', buffered: true});
} catch (error) {
// Do nothing if the browser doesn't support this API.
}
API de tiempo de recursos #
La API Resource Timing (Tiempo de recursos) ofrece a los desarrolladores información detallada sobre cómo se cargaron los recursos para 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 mucho de eso). Otros datos a los que se pueden acceder incluyen:
- InitiatorType: cómo se obtuvo el recurso, por ejemplo, de una etiqueta de
<script>
o<link>
, o defetch()
- nextHopProtocol: el protocolo utilizado para obtener el recurso, como
h2
oquic
- encodedBodySize/decodedBodySize]: el tamaño del recurso en su forma codificada o decodificada (respectivamente)
- transferSize: el tamaño del recurso que realmente se transfirió a través de la red. Cuando los recursos se completan a través de la caché, este valor puede ser mucho menor que
encodedBodySize
y en algunos casos, puede ser cero (si no se requiere una revalidación de la caché).
Ten en cuenta que puedes utilizar la propiedad de transferSize
de las entradas de tiempo de recursos para medir una métrica de tasa de aciertos de caché o incluso 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 habituales.
El siguiente ejemplo registra todos los recursos consultados por la página e indica si cada recurso se completó o no a través de la caché.
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled via the cache.
console.log(entry.name, entry.transferSize === 0);
}
});
// Start listening for `resource` entries to be dispatched.
po.observe({type: 'resource', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
API de Navigation Timing #
La API de Navigation Timing (Tiempo de navegación) es similar a la API de Resource Timing, pero solo informa sobre las consultas de navegación. El tipo de entrada de navigation
también es similar al tipo de entrada de resource
, pero contiene información adicional específica solo para las consultas de navegación (como cuando se disparan los eventos de DOMContentLoaded
y load
).
Una métrica que muchos desarrolladores siguen para comprender el tiempo de respuesta del servidor (Time to First Byte (TTFB): Tiempo hasta el primer byte) está disponible a través de la API de Navigation Timing, específicamente, la marca de tiempo de responseStart
.
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled via the cache.
console.log('Time to first byte', entry.responseStart);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Otra métrica que les puede interesar a los desarrolladores de métricas que utilizan el service worker es el tiempo de inicio del service worker para las consultas de navegación. Esta es la cantidad de tiempo que le toma al navegador iniciar el subproceso del service worker antes de que pueda comenzar a interceptar eventos de fetch (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
.
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
API de Server Timing #
La API de Server Timing (Tiempo del servidor) te permite pasar datos de tiempos específicos de la solicitud desde tu servidor al navegador a través de cabeceras de respuesta. Por ejemplo, se puede indicar cuánto tiempo se 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 en el servidor.
Para los desarrolladores que utilizan proveedores de análisis de terceros, la API de Server Timing es la única forma de correlacionar los datos de rendimiento del servidor con otras métricas comerciales que estas herramientas de análisis pueden estar midiendo.
Para especificar los datos de tiempo del servidor en sus respuestas, puedes usar la cabecera de respuesta de Server-Timing
. Aquí 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 resource
o en navigation
de las API de Resource Timing y Navigation Timing respectivamente.
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}