Supervisa el uso total de memoria de tu página web con MeasureUserAgentSpecificMemory()

Aprende a medir el uso de memoria de tu página web en producción para detectar regresiones.

Brendan Kenny
Brendan Kenny
Ulán Degenbáv
Ulán Degenbaev

Los navegadores administran la memoria de las páginas web automáticamente. Cada vez que una página web crea un objeto, el navegador asigna un fragmento de memoria “de forma interna” para almacenarlo. Dado que la memoria es un recurso finito, el navegador realiza la recolección de elementos no utilizados para detectar cuándo un objeto ya no es necesario y liberar el fragmento de memoria subyacente.

Sin embargo, la detección no es perfecta y se comprobó que esa tarea es imposible. Por lo tanto, los navegadores se aproximan a la noción de “se necesita un objeto” con la noción de “se puede acceder a un objeto”. Si la página web no puede acceder a un objeto a través de sus variables ni los campos de otros objetos accesibles, el navegador puede reclamar el objeto de forma segura. La diferencia entre estas dos nociones genera fugas de memoria, como se ilustra en el siguiente ejemplo.

const object = {a: new Array(1000), b: new Array(2000)};
setInterval(() => console.log(object.a), 1000);

En este caso, ya no se necesita el array más grande, b, pero el navegador no lo recupera porque aún se puede acceder a él a través de object.b en la devolución de llamada. Por lo tanto, se filtra la memoria del array más grande.

Las fugas de memoria son prevalentes en la Web. Es fácil ingresar uno si olvidas cancelar el registro de un objeto de escucha de eventos, puedes capturar objetos de un iframe por accidente, no cerrar un trabajador, acumular objetos en arrays, etcétera. Si una página web tiene fugas de memoria, su uso aumenta con el tiempo y parece que la página web es lenta y está sobrecargada para los usuarios.

El primer paso para resolver este problema es medirlo. La nueva API de performance.measureUserAgentSpecificMemory() permite a los desarrolladores medir el uso de memoria de sus páginas web en producción y, por lo tanto, detectar fugas de memoria que se omiten en las pruebas locales.

¿En qué se diferencia performance.measureUserAgentSpecificMemory() de la API de performance.memory heredada?

Si estás familiarizado con la API de performance.memory no estándar existente, es posible que te preguntes en qué se diferencia la API nueva. La diferencia principal es que la API anterior muestra el tamaño del montón de JavaScript, mientras que la API nueva estima la memoria que usa la página web. Esta diferencia se vuelve importante cuando Chrome comparte el mismo montón con varias páginas web (o varias instancias de la misma página web). En esos casos, el resultado de la API anterior puede ser arbitrariamente desactivado. Dado que la API anterior se define en términos específicos de la implementación, como "montón", no tiene posibilidades de estandarizarla.

Otra diferencia es que la nueva API realiza una medición de memoria durante la recolección de elementos no utilizados. Esto reduce el ruido en los resultados, pero puede tardar un tiempo en generarse los resultados. Ten en cuenta que otros navegadores pueden decidir implementar la nueva API sin depender de la recolección de elementos no utilizados.

Casos de uso sugeridos

El uso de memoria de una página web depende del tiempo de los eventos, las acciones del usuario y las recolecciones de elementos no utilizados. Es por eso que la API de medición de memoria está diseñada para agregar datos de uso de memoria provenientes de la producción. Los resultados de llamadas individuales son menos útiles. Casos prácticos de ejemplo:

  • Detección de regresión durante el lanzamiento de una nueva versión de la página web para detectar nuevas fugas de memoria.
  • Realiza pruebas A/B de una función nueva para evaluar su impacto en la memoria y detectar fugas de memoria.
  • Correlaciona el uso de memoria con la duración de la sesión para verificar la presencia o ausencia de fugas de memoria.
  • Correlaciona el uso de memoria con las métricas del usuario para comprender el impacto general del uso de memoria.

Compatibilidad del navegador

Navegadores compatibles

  • 89
  • 89
  • x
  • x

Origen

Actualmente, la API solo es compatible con los navegadores basados en Chromium a partir de Chrome 89. El resultado de la API depende en gran medida de la implementación porque los navegadores tienen diferentes formas de representar los objetos en la memoria y maneras diferentes de estimar el uso de memoria. Es posible que los navegadores excluyan algunas regiones de la memoria de los datos si la contabilización adecuada es demasiado costosa o inviable. Por lo tanto, no se pueden comparar los resultados en distintos navegadores. Solo es significativo comparar los resultados para el mismo navegador.

Usa performance.measureUserAgentSpecificMemory()

Detección de funciones

La función performance.measureUserAgentSpecificMemory no estará disponible o puede fallar con un SecurityError si el entorno de ejecución no cumple con los requisitos de seguridad para evitar filtraciones de información de origen cruzado. Se basa en el aislamiento de origen cruzado, que una página web puede activar mediante la configuración de los encabezados COOP+COEP.

La compatibilidad se puede detectar en el tiempo de ejecución:

if (!window.crossOriginIsolated) {
  console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
} else if (!performance.measureUserAgentSpecificMemory) {
  console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
} else {
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
    } else {
      throw error;
    }
  }
  console.log(result);
}

Pruebas locales

Chrome realiza la medición de memoria durante la recolección de elementos no utilizados, lo que significa que la API no resuelve la promesa de resultado de inmediato y, en cambio, espera la próxima recolección.

Llamar a la API fuerza una recolección de elementos no utilizados después de un tiempo de espera, que actualmente se establece en 20 segundos, aunque puede ocurrir antes. Iniciar Chrome con la marca de línea de comandos --enable-blink-features='ForceEagerMeasureMemory' reduce el tiempo de espera a cero y es útil para depuración y pruebas locales.

Ejemplo

El uso recomendado de la API es definir un monitor de memoria global que muestre el uso de memoria de toda la página web y envíe los resultados a un servidor para su agregación y análisis. La forma más sencilla es realizar muestras periódicas, por ejemplo, cada M minutos. Sin embargo, eso genera sesgo en los datos porque pueden producirse picos de memoria entre las muestras.

En el siguiente ejemplo, se muestra cómo realizar mediciones de memoria imparciales mediante un proceso Poisson, que garantiza que las muestras tengan la misma probabilidad de ocurrir en cualquier momento (demostración, fuente).

Primero, define una función que programe la siguiente medición de memoria mediante setTimeout() con un intervalo aleatorizado.

function scheduleMeasurement() {
  // Check measurement API is available.
  if (!window.crossOriginIsolated) {
    console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
    console.log('See https://web.dev/coop-coep/ to learn more')
    return;
  }
  if (!performance.measureUserAgentSpecificMemory) {
    console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
    return;
  }
  const interval = measurementInterval();
  console.log(`Running next memory measurement in ${Math.round(interval / 1000)} seconds`);
  setTimeout(performMeasurement, interval);
}

La función measurementInterval() calcula un intervalo aleatorio en milisegundos, de modo que, en promedio, haya una medición cada cinco minutos. Consulta Distribución exponencial si te interesan las matemáticas detrás de la función.

function measurementInterval() {
  const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000;
  return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS;
}

Por último, la función performMeasurement() asíncrona invoca la API, registra el resultado y programa la siguiente medición.

async function performMeasurement() {
  // 1. Invoke performance.measureUserAgentSpecificMemory().
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
      return;
    }
    // Rethrow other errors.
    throw error;
  }
  // 2. Record the result.
  console.log('Memory usage:', result);
  // 3. Schedule the next measurement.
  scheduleMeasurement();
}

Finalmente, empieza a medir.

// Start measurements.
scheduleMeasurement();

El resultado puede ser el siguiente:

// Console output:
{
  bytes: 60_100_000,
  breakdown: [
    {
      bytes: 40_000_000,
      attribution: [{
        url: 'https://example.com/',
        scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 20_000_000,
      attribution: [{
          url: 'https://example.com/iframe',
          container: {
            id: 'iframe-id-attribute',
            src: '/iframe',
          },
          scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 100_000,
      attribution: [],
      types: ['DOM']
    },
  ],
}

La estimación del uso total de memoria se muestra en el campo bytes. Este valor depende en gran medida de la implementación y no se puede comparar en otros navegadores. Incluso puede cambiar entre diferentes versiones del mismo navegador. El valor incluye la memoria de JavaScript y DOM de todos los iframes, las ventanas relacionadas y los trabajadores web en el proceso actual.

La lista breakdown proporciona más información sobre la memoria usada. Cada entrada describe parte de la memoria y la atribuye a un conjunto de ventanas, iframes y trabajadores identificados por URL. En el campo types, se enumeran los tipos de memoria específicos de la implementación asociados con la memoria.

Es importante tratar todas las listas de manera genérica y no codificar las suposiciones basadas en un navegador en particular. Por ejemplo, algunos navegadores pueden mostrar un breakdown vacío o un attribution vacío. Otros navegadores pueden mostrar varias entradas en attribution que indican que no pudieron distinguir cuál de estas entradas es propietaria de la memoria.

Comentarios

Al Grupo de la Comunidad de rendimiento web y al equipo de Chrome les encantaría conocer tus opiniones y experiencias con performance.measureUserAgentSpecificMemory().

Cuéntanos sobre el diseño de la API

¿Hay algo acerca de la API que no funciona como se espera? ¿O faltan propiedades que necesitas para implementar tu idea? Informa sobre un problema de especificaciones en el repositorio de GitHub performance.measureUserAgentSpecificMemory() o agrega lo que piensas a un problema existente.

Informar un problema con la implementación

¿Encontraste un error en la implementación de Chrome? ¿La implementación es diferente de las especificaciones? Informa un error en new.crbug.com. Asegúrate de incluir todos los detalles que puedas, brinda instrucciones simples para reproducir el error y configura los Componentes como Blink>PerformanceAPIs. Glitch funciona muy bien para compartir repros rápidos y fáciles.

Muestra tu apoyo

¿Planeas usar performance.measureUserAgentSpecificMemory()? La asistencia pública ayuda al equipo de Chrome a priorizar funciones y le muestra a otros proveedores de navegadores la importancia de admitirlas. Envía un tweet a @ChromiumDev y cuéntanos dónde y cómo lo usas.

Vínculos útiles

Agradecimientos

Muchas gracias a Domenic Denicola, Yoav Weiss, Mathias Bynens por las revisiones de diseño de APIs, y a Dominik Inführ, Hannes Payer, Kentaro Hara y Michael Lippautz por las revisiones de código en Chrome. También agradezco a Per Parker, Philipp Weis, Olga Belomestnykh, Matthew Bolohan y Neil Mckay por proporcionar comentarios valiosos de los usuarios que mejoraron significativamente la API.

Hero image de Harrison Broadbent en Unsplash