La API de Rendimiento mide la capacidad de respuesta de tu aplicación web en tiempo real en dispositivos con usuarios reales y conexiones de red. Puede ayudar a identificar cuellos de botella en tu código del lado del cliente y del lado del servidor con:

  • user timing: Medición personalizada del rendimiento de las funciones JavaScript del lado del cliente
  • paint timing: Métricas de renderizado del navegador
  • resource timing: Rendimiento de la carga de activos y de las llamadas Ajax
  • navigation timing: Métricas de carga de la página, incluidas las redirecciones, las búsquedas de DNS, la disponibilidad del DOM, etc

La API aborda varios problemas asociados a la típica evaluación del rendimiento:

  1. Los desarrolladores suelen probar las aplicaciones en ordenadores de gama alta conectados a una red rápida. DevTools puede emular dispositivos más lentos, pero no siempre mostrará los problemas del mundo real cuando la mayoría de los clientes utilizan un móvil de hace más de dos años conectado al WiFi del aeropuerto.
  2. Las opciones de terceros, como Google Analytics, suelen estar bloqueadas, lo que da lugar a resultados y suposiciones sesgadas. También puedes encontrarte con implicaciones de privacidad en algunos países.
  3. La API de rendimiento puede medir con precisión varias métricas mejor que métodos como Date().


Las siguientes secciones describen las formas en que puedes utilizar la API de Rendimiento. Se recomienda tener algunos conocimientos de JavaScript y de métricas de carga de páginas.

Disponibilidad de la API de Rendimiento

La mayoría de los navegadores actuales son compatibles con la API de rendimiento, incluidos IE10 e IE11 (incluso IE9 tiene una compatibilidad limitada). Puedes detectar la presencia de la API utilizando:

if ('performance' in window) {
  // use Performance API
}

No es posible rellenar completamente la API, así que ten cuidado con los navegadores que faltan. Si el 90% de tus usuarios navegan felizmente con Internet Explorer 8, sólo estarías midiendo el 10% de los clientes con aplicaciones más potentes.

La API puede utilizarse en Web Workers, que proporcionan una forma de ejecutar cálculos complejos en un hilo de fondo sin detener las operaciones del navegador.

La mayoría de los métodos de la API pueden utilizarse en Node.js del lado del servidor con el módulo perf_hooks estándar:

// Node.js performance
import { performance } from 'node:perf_hooks';
// or in Common JS: const { performance } = require('node:perf_hooks');

console.log( performance.now() );

Deno proporciona la API de Rendimiento estándar:

// Deno performance
console.log( performance.now() );

Necesitarás ejecutar scripts con el permiso --allow-hrtime para habilitar la medición del tiempo de alta resolución:

deno run --allow-hrtime index.js

El rendimiento del lado del servidor suele ser más fácil de evaluar y gestionar porque depende de la carga, las CPU, la RAM, los discos duros y los límites del cloud services. Las actualizaciones de hardware o las opciones de gestión de procesos como PM2, clustering y Kubernetes pueden ser más eficaces que la refactorización del código.

Por esta razón, las siguientes secciones se centran en el rendimiento del lado del cliente.

Medición Personalizada del Rendimiento

La API de Rendimiento puede utilizarse para cronometrar la velocidad de ejecución de las funciones de tu aplicación. Es posible que hayas utilizado o encontrado funciones de cronometraje utilizando Date():

const timeStart = new Date();
runMyCode();
const timeTaken = new Date() - timeStart;

console.log(`runMyCode() executed in ${ timeTaken }ms`);

La API de rendimiento ofrece dos ventajas principales:

  1. Mejor precisión: Date() mide al milisegundo más cercano, pero la API de rendimiento puede medir fracciones de milisegundo (dependiendo del navegador).
  2. Mayor fiabilidad: El usuario o el sistema operativo pueden cambiar la hora del sistema, por lo que las métricas basadas en Date() no siempre serán precisas. Esto significa que ¡tus funciones pueden parecer especialmente lentas cuando los relojes se adelantan!

El equivalente a Date() es performance.now() que devuelve una marca de tiempo de alta resolución que se pone a cero cuando se inicia el proceso responsable de crear el documento (la página se ha cargado):

const timeStart = performance.now();
runMyCode();
const timeTaken = performance.now() - timeStart;

console.log(`runMyCode() executed in ${ timeTaken }ms`);

Una propiedad no estándar performance.timeOrigin también puede devolver una marca de tiempo del 1 de enero de 1970, aunque no está disponible en IE y Deno.

performance.now() se vuelve poco práctico cuando se realizan algo más de unas pocas mediciones. La API de rendimiento proporciona un búfer en el que puedes registrar los eventos para su posterior análisis, pasando un nombre de etiqueta a performance.mark():

performance.mark('start:app');
performance.mark('start:init');

init(); // run initialization functions

performance.mark('end:init');
performance.mark('start:funcX');

funcX(); // run another function

performance.mark('end:funcX');
performance.mark('end:app');

Se puede extraer un aaray de todos los objetos de marca en el buffer de Rendimiento utilizando:

const mark = performance.getEntriesByType('mark');

Ejemplo de resultado:

[

  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "start:app"
    startTime: 1000
  },
  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "start:init"
    startTime: 1001
  },
  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "end:init"
    startTime: 1100
  },
...
]

El método performance.measure() calcula el tiempo entre dos marcas y también lo almacena en el buffer de Rendimiento. Pasas un nuevo nombre de medida, el nombre de la marca inicial (o null para medir desde la carga de la página) y el nombre de la marca final (o null para medir hasta la hora actual):

performance.measure('init', 'start:init', 'end:init');

Se añade al buffer un objeto PerformanceMeasure con el tiempo de duración calculado. Para obtener este valor, puedes solicitar un array de todas las medidas:

const measure = performance.getEntriesByType('measure');

o solicitar una medida por su nombre:

performance.getEntriesByName('init');

Ejemplo de resultado:

[
  {
    detail: null
    duration: 99
    entryType: "measure"
    name: "init"
    startTime: 1001
  }
]

Uso del Buffer de Rendimiento

Además de las marcas y las medidas, el búfer de Rendimiento se utiliza para registrar automáticamente el navigation timing, resource timing y paint timing (del que hablaremos más adelante). Puedes obtener un array con todas las entradas del búfer:

performance.getEntries();

Por defecto, la mayoría de los navegadores proporcionan un búfer que almacena hasta 150 métricas de recursos. Esto debería ser suficiente para la mayoría de las evaluaciones, pero puedes aumentar o disminuir el límite del búfer si es necesario:

// record 500 metrics
performance.setResourceTimingBufferSize(500);

Las marcas se pueden borrar por nombre o puedes especificar un valor vacío para borrar todas las marcas:

performance.clearMarks('start:init');

Del mismo modo, las medidas pueden borrarse por su nombre o con un valor vacío para borrarlas todas:

performance.clearMeasures();

Monitorización de las Actualizaciones del Búfer de Rendimiento

PerformanceObserver puede monitorizar los cambios en el buffer de Rendimiento y ejecutar una función cuando se produzcan eventos específicos. La sintaxis te resultará familiar si has utilizado MutationObserver para responder a las actualizaciones del DOM o IntersectionObserver para detectar cuando los elementos se desplazan en la ventana gráfica.

Debes definir una función observadora con dos parámetros:

  1. un array de entradas de observadores que se han detectado, y
  2. el objeto observador. Si es necesario, se puede llamar a su método disconnect() para detener el observador.
function performanceCallback(list, observer) {

  list.getEntries().forEach(entry => {
    console.log(`name    : ${ entry.name }`);
    console.log(`type    : ${ entry.type }`);
    console.log(`start   : ${ entry.startTime }`);
    console.log(`duration: ${ entry.duration }`);
  });

}

La función se pasa a un nuevo objeto PerformanceObserver. A su método observe() se le pasa un array de tipos de entrada del búfer de Rendimiento para observar:

let observer = new PerformanceObserver( performanceCallback );
observer.observe({ entryTypes: ['mark', 'measure'] });

En este ejemplo, al añadir una nueva marca o medida se ejecuta la función performanceCallback(). Aunque aquí sólo se registran los mensajes, podría utilizarse para activar una carga de datos o realizar otros cálculos.

Medir el Paint Performance

La API de Paint Timing sólo está disponible en JavaScript del lado del cliente y registra automáticamente dos métricas que son importantes para Core Web Vitals:

  1. first-paint: El navegador ha comenzado a dibujar la página.
  2. first-contentful-paint: El navegador ha pintado el primer elemento significativo del contenido del DOM, como un encabezado o una imagen.

Éstas pueden extraerse del buffer de Rendimiento a un array:

const paintTimes = performance.getEntriesByType('paint');

Ten cuidado con ejecutar esto antes de que la página se haya cargado completamente; los valores no estarán listos. Espera al evento window.load o utiliza un PerformanceObserver para controlar paint entryTypes.

Ejemplo de resultado:

[
  {
    "name": "first-paint",
    "entryType": "paint",
    "startTime": 812,
    "duration": 0
  },
  {
    "name": "first-contentful-paint",
    "entryType": "paint",
    "startTime": 856,
    "duration": 0
  }
]

Una first-paint lenta suele estar causada por un CSS o un JavaScript que bloquea la renderización. El desfase con respecto a first-contentful-paint puede ser grande si el navegador tiene que descargar una imagen de gran tamaño o renderizar elementos complejos.

Medición del Rendimiento de los Recursos

Los tiempos de red de recursos como imágenes, hojas de estilo y archivos JavaScript se registran automáticamente en el buffer de rendimiento. Aunque es poco lo que puedes hacer para resolver los problemas de velocidad de la red (aparte de reducir el tamaño de los archivos), puede ayudar a poner de manifiesto los problemas con los activos más grandes, las respuestas Ajax lentas o los scripts de terceros que funcionan mal.

Se puede extraer un array de métricas PerformanceResourceTiming del búfer utilizando:

const resources = performance.getEntriesByType('resource');

Alternativamente, puedes obtener las métricas de un activo pasando su URL completa:

const resource = performance.getEntriesByName('https://test.com/script.js');

Ejemplo de resultado:

[
  {
    connectEnd: 195,
    connectStart: 195,
    decodedBodySize: 0,
    domainLookupEnd: 195,
    domainLookupStart: 195,
    duration: 2,
    encodedBodySize: 0,
    entryType: "resource",
    fetchStart: 195,
    initiatorType: "script",
    name: "https://test.com/script.js",
    nextHopProtocol: "h3",
    redirectEnd: 0,
    redirectStart: 0,
    requestStart: 195,
    responseEnd: 197,
    responseStart: 197,
    secureConnectionStart: 195,
    serverTiming: [],
    startTime: 195,
    transferSize: 0,
    workerStart: 195
  }
]

Se pueden examinar las siguientes propiedades:

  • name: La URL del recurso
  • entryType: «recurso»
  • initiatorType: Cómo se inició el recurso, como «script» o «enlace»
  • serverTiming: Un array de PerformanceServerTiming objetos pasados por el servidor en la cabecera HTTP Server-Timing (tu aplicación del lado del servidor podría enviar métricas al cliente para su posterior análisis)
  • startTime: Fecha de inicio de la búsqueda
  • nextHopProtocol: Protocolo de red utilizado
  • workerStart: Marca de tiempo antes de iniciar un Progressive Web App Service Worker (0 si la solicitud no es interceptada por un Service Worker)
  • redirectStart: Marca de tiempo cuando se inició una redirección
  • redirectEnd: Marca de tiempo después del último byte de la última respuesta de redirección
  • fetchStart: Marca de tiempo antes de la obtención del recurso
  • domainLookupStart: Marca de tiempo antes de una búsqueda de DNS
  • domainLookupEnd: Marca de tiempo después de la búsqueda de DNS
  • connectStart: Marca de tiempo antes de establecer una conexión con el servidor
  • connectEnd: Marca de tiempo después de establecer una conexión con el servidor
  • secureConnectionStart: Marca de tiempo antes del handshake SSL
  • requestStart: Marca de tiempo antes de que el navegador solicite el recurso
  • responseStart: Marca de tiempo cuando el navegador recibe el primer byte de datos
  • responseEnd: Marca de tiempo después de recibir el último byte o de cerrar la conexión
  • duration: La diferencia entre startTime y responseEnd
  • transferSize: El tamaño del recurso en bytes, incluyendo la cabecera y el cuerpo comprimido
  • encodedBodySize: El cuerpo del recurso en bytes antes de la descompresión
  • decodedBodySize: El cuerpo del recurso en bytes después de la descompresión

Este script de ejemplo recupera todas las peticiones Ajax iniciadas por la API Fetch y devuelve el tamaño y la duración total de la transferencia:

const fetchAll = performance.getEntriesByType('resource')
  .filter( r => r.initiatorType === 'fetch')
  .reduce( (sum, current) => {
    return {
      transferSize: sum.transferSize += current.transferSize,
      duration: sum.duration += current.duration
    }
  },
  { transferSize: 0, duration: 0 }
);

Medición del Rendimiento de la Navegación

Los tiempos de red para la descarga de la página anterior y la carga de la página actual se registran automáticamente en el buffer de Rendimiento como un único PerformanceNavigationTiming objeto.

Extráelo a un array utilizando:

const pageTime = performance.getEntriesByType('navigation');

…o pasando la URL de la página a .getEntriesByName():

const pageTiming = performance.getEntriesByName(window.location);

Las métricas son idénticas a las de los recursos, pero también incluyen valores específicos de la página:

  • entryType: Por ejemplo, «navigation»
  • type: Ya sea «navigate», «reload», «back_forward» o «prerender»
  • redirectCount: El número de redirecciones
  • unloadEventStart: Marca de tiempo antes del evento de descarga del documento anterior
  • unloadEventEnd: Marca de tiempo después del evento de descarga del documento anterior
  • domInteractive: Marca de tiempo cuando el navegador ha analizado el HTML y construido el DOM
  • domContentLoadedEventStart: Marca de tiempo antes de que se dispare el evento DOMContentLoaded del documento
  • domContentLoadedEventEnd: Marca de tiempo después de que finalice el evento DOMContentLoaded del documento
  • domComplete: Marca de tiempo después de que la construcción del DOM y los eventos DOMContentLoaded hayan finalizado
  • loadEventStart: Marca de tiempo antes de que se dispare el evento de carga de la página
  • loadEventEnd: Marca de tiempo después del evento de carga de la página y de que todos los activos estén disponibles

Los problemas típicos son:

  • Un gran retraso entre unloadEventEnd y domInteractive. Esto podría indicar una respuesta lenta del servidor.
  • Un gran retraso entre domContentLoadedEventStart y domComplete. Esto podría indicar que los scripts de inicio de la página son demasiado lentos.
  • Un gran retraso entre domComplete y loadEventEnd. Esto podría indicar que la página tiene demasiados activos o que varios están tardando demasiado en cargarse.

Registro y Análisis del Rendimiento

La API de rendimiento te permite recopilar datos de uso del mundo real y subirlos a un servidor para su posterior análisis. Podrías utilizar un servicio de terceros, como Google Analytics, para almacenar los datos, pero existe el riesgo de que el script de terceros se bloquee o introduzca nuevos problemas de rendimiento. Tu propia solución puede adaptarse a tus necesidades para garantizar que la monitorización no afecte a otras funciones.

Ten cuidado con las situaciones en las que no se pueden determinar las estadísticas, quizás porque los usuarios tienen navegadores antiguos, bloquean JavaScript o están detrás de un proxy corporativo. Entender qué datos faltan puede ser más fructífero que hacer suposiciones basadas en información incompleta.

Lo ideal es que tus scripts de análisis no afecten negativamente al rendimiento al ejecutar cálculos complejos o cargar grandes cantidades de datos. Considera la posibilidad de utilizar web workers y minimizar el uso de llamadas sincrónicas a localStorage. Siempre es posible procesar por lotes los datos en bruto más tarde.

Por último, ten cuidado con los valores atípicos, como dispositivos y conexiones muy rápidos o muy lentos, que afectan negativamente a las estadísticas. Por ejemplo, si nueve usuarios cargan una página en dos segundos, pero el décimo experimenta una descarga de 60 segundos, la latencia media se eleva a casi 8 segundos. Una métrica más realista es la cifra media (2 segundos) o el percentil 90 (9 de cada 10 usuarios experimentan un tiempo de carga de 2 segundos o menos).

Resumen

El rendimiento de la web sigue siendo un factor crítico para los desarrolladores. Los usuarios esperan que los sitios y las aplicaciones sean responsivos en la mayoría de los dispositivos. La optimización de los motores de búsqueda también puede verse afectada, ya que los sitios más lentos baja de posición en Google.

Hay un montón de herramientas de monitorización del rendimiento, pero la mayoría evalúan la velocidad de ejecución del lado del servidor o utilizan un número limitado de clientes capaces de juzgar el rendimiento del navegador. La API de rendimiento proporciona una forma de cotejar las métricas reales de los usuarios que no sería posible calcular de ninguna otra forma.

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.