Aprende a medir el uso de memoria de tu página web en producción para detectar regresiones.
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 "en segundo plano" para almacenarlo. Dado que la memoria es un recurso finito, el navegador realiza una 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 el problema de la parada de Alan Turing 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 alcanzar un objeto a través de sus variables y los campos de otros objetos alcanzables, 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);
Aquí, el array más grande b ya no es necesario, 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 pierde la memoria del array más grande.
Las fugas de memoria son frecuentes en la Web, como se puede ver en este estudio. Es fácil introducir una fuga de memoria si olvidas cancelar el registro de un objeto de escucha de eventos, si capturas objetos de un iframe por accidente, 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 parece lenta y sobrecargada para los usuarios.
El primer paso para resolver este problema es medirlo. La nueva API de performance.measureUserAgentSpecificMemory() permite que los desarrolladores midan el uso de memoria de sus páginas web en producción y, de esta manera, detecten fugas de memoria que se pasan por alto en las pruebas locales.
¿En qué se diferencia performance.measureUserAgentSpecificMemory() de la API de performance.memory heredada?
Si conoces la API de performance.memory no estándar existente,
es posible que te preguntes en qué se diferencia de la nueva API. La diferencia principal es
que la API anterior devuelve 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 ser arbitrariamente incorrecto. Dado que la API anterior se define en términos específicos de la implementación, como "montón", estandarizarla es inútil.
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 puede tardar un tiempo hasta que se produzcan 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 momento en que se producen los eventos, las acciones del usuario y las recolecciones de elementos no utilizados. Por ese motivo, 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. Ejemplos de casos de uso:
- Detección de regresión durante el lanzamiento de una nueva versión de la página web para detectar nuevas fugas de memoria
- Realizar pruebas A/B de una función nueva para evaluar su impacto en la memoria y detectar fugas de memoria
- Correlacionar el uso de memoria con la duración de la sesión para verificar la presencia o ausencia de fugas de memoria
- Correlacionar el uso de memoria con las métricas del usuario para comprender el impacto general del uso de memoria
Compatibilidad del navegador
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, ya que los navegadores tienen diferentes formas de representar objetos en la memoria y diferentes formas de estimar el uso de memoria. Los navegadores pueden excluir algunas regiones de memoria de la contabilidad si la contabilidad adecuada es demasiado costosa o inviable. Por lo tanto, los resultados no se pueden comparar entre navegadores. Solo tiene sentido comparar los resultados para el mismo navegador.
Usa performance.measureUserAgentSpecificMemory()
Detección de atributos
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
configurando los encabezados COOP y 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 función experimental 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
tome muestras del 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 un muestreo periódico, por
ejemplo, cada M minutos. Sin embargo, esto introduce un sesgo en los datos,
ya que pueden producirse picos de memoria entre las muestras.
En el siguiente ejemplo, se muestra cómo realizar mediciones de memoria imparciales con un proceso de Poisson, que garantiza que las muestras tengan la misma probabilidad de ocurrir en cualquier momento.
Primero, define una función que programe la próxima 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 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 asíncrona performMeasurement() 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 medir.
// Start measurements.
scheduleMeasurement();
El resultado puede 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 de memoria total se devuelve 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 del 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 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 de forma rígida las suposiciones basadas en un navegador en particular. Por ejemplo, algunos navegadores pueden
devolver un breakdown vacío o un attribution vacío. Otros navegadores pueden
devolver varias entradas en attribution, lo que indica 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? Presenta un problema de especificación en el repositorio de GitHub de performance.measureUserAgentSpecificMemory() o agrega tus comentarios a un problema existente.
Informa 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 sencillas para reproducir el error y establecer Components en Blink>PerformanceAPIs.
Muestra tu apoyo
¿Tienes pensado 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 tweet a @ChromiumDev
y cuéntanos dónde y cómo lo usas.
Vínculos útiles
- Explicativa
- Error de seguimiento
- Entrada de ChromeStatus.com
- Cambios desde la API de prueba de origen
- Prueba de origen finalizada
Agradecimientos
Muchas gracias a Domenic Denicola, Yoav Weiss y Mathias Bynens por las revisiones del 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 en gran medida la API.