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

Descubre cómo medir el uso de memoria de tu página web en producción para detectar regresiones.

Ulan Degenbaev
Ulan 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 “debajo del capó” para almacenarlo. Dado que la memoria es un recurso finito, el navegador realiza la recolección de elementos no usados 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 demostró que la detección perfecta es una tarea imposible. Por lo tanto, los navegadores aproximan 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 y los campos de otros objetos accesibles, el navegador puede recuperar 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);

Aquí ya no se necesita el array más grande b, pero el navegador no lo reclama 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 muy comunes en la Web. Es fácil introducir uno si te olvidas de anular el registro de un objeto de escucha de eventos, si capturas objetos accidentalmente desde un iframe, si no cierras un trabajador, si acumulas objetos en arrays, etcétera. Si una página web tiene fugas de memoria, su uso de memoria aumenta con el tiempo y la página web les parece lenta y sobrecargada a 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 la memoria de sus páginas web en producción y, así, detectar las fugas de memoria que se escapan de las pruebas locales.

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

Si conoces la API de performance.memory no estándar existente, es posible que te preguntes en qué se diferencia la nueva API. La principal diferencia es que la API anterior muestra el tamaño del montón de JavaScript, mientras que la nueva API 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 estar desactivado de forma arbitraria. Dado que la API anterior se define en términos específicos de la implementación, como "heap", estandarizarla es imposible.

Otra diferencia es que la nueva API realiza la medición de la memoria durante la recolección de elementos no utilizados. Esto reduce el ruido en los resultados, pero es posible que debas esperar un mientras hasta que se produzcan. 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 reconstrucciones 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 de producción. Los resultados de las 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
  • Prueba A/B de una función nueva para evaluar su impacto en la memoria y detectar fugas de memoria
  • Correlacionar el uso de la memoria con la duración de la sesión para verificar la presencia o ausencia de fugas de memoria
  • Correlacionar el uso de la memoria con las métricas del usuario para comprender el impacto general del uso de la memoria

Compatibilidad del navegador

Navegadores compatibles

  • Chrome: 89.
  • Edge: 89.
  • Firefox: No es compatible.
  • Safari: No se admite.

Origen

Actualmente, la API solo es compatible con navegadores basados en Chromium, a partir de Chrome 89. El resultado de la API depende en gran medida de la implementación, ya que los navegadores tienen diferentes formas de representar objetos en la memoria y diferentes formas de estimar el uso de la memoria. Los navegadores pueden excluir algunas regiones de memoria de la contabilización si la contabilización adecuada es demasiado costosa o inviable. Por lo tanto, los resultados no se pueden comparar entre navegadores. Solo tiene sentido comparar los resultados del mismo navegador.

Usa performance.measureUserAgentSpecificMemory()

Detección de atributos

La función performance.measureUserAgentSpecificMemory no estará disponible o podría fallar con un SecurityError si el entorno de ejecución no cumple con los requisitos de seguridad para evitar filtraciones de información entre orígenes. Se basa en el aislamiento multiorigen, que una página web puede activar configurando los encabezados COOP+COEP.

La compatibilidad se puede detectar durante 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 la 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 su lugar, espera a la siguiente recolección de elementos no utilizados.

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 la depuración y las 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 tomar muestras de forma periódica, por ejemplo, cada M minutos. Sin embargo, eso introduce sesgos en los datos, ya que pueden ocurrir picos de memoria entre los muestreos.

En el siguiente ejemplo, se muestra cómo realizar mediciones de memoria imparciales con un proceso de Poisson, que garantiza que los muestreos tengan la misma probabilidad de ocurrir en cualquier momento (demo, fuente).

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

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 interesa la matemática 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();
}

Por último, comienza a realizar mediciones.

// Start measurements.
scheduleMeasurement();

El resultado podría verse de la siguiente manera:

// 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 entre 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 utilizada. Cada entrada describe una 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 forma genérica y no codificar las suposiciones en función de un navegador en particular. Por ejemplo, algunos navegadores pueden mostrar un breakdown o un attribution vacíos. Es posible que otros navegadores muestren varias entradas en attribution que indiquen 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 en la API que no funciona como se espera? ¿O faltan propiedades que necesitas para implementar tu idea? Informa un problema de especificación en el repositorio de GitHub de performance.measureUserAgentSpecificMemory() o agrega tus comentarios a un problema existente.

Denuncia un problema con la implementación

¿Encontraste un error en la implementación de Chrome? ¿O la implementación es diferente de la especificación? Informa un error en new.crbug.com. Asegúrate de incluir la mayor cantidad de detalles posible, proporcionar instrucciones simples para reproducir el error y establecer Componentes en Blink>PerformanceAPIs. Glitch es excelente para compartir reproducciones rápidas y fáciles.

Expresar apoyo

¿Piensas usar performance.measureUserAgentSpecificMemory()? Tu apoyo público ayuda al equipo de Chrome a priorizar las funciones y les muestra a otros proveedores de navegadores lo importante que es admitirlas. Envía un tuit a @ChromiumDev y cuéntanos dónde y cómo lo usas.

Vínculos útiles

Agradecimientos

Muchas gracias a Domenic Denicola, Yoav Weiss y Mathias Bynens por las revisiones de diseño de la API, 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 mucho la API.

Imagen hero de Harrison Broadbent en Unsplash