Combatir pérdidas de memoria en aplicaciones web

Cuando pasamos del desarrollo de sitios web cuyas páginas se forman en el servidor a la creación de aplicaciones web de una sola página que se representan en el cliente, adoptamos ciertas reglas del juego. Uno de ellos es el manejo preciso de los recursos en el dispositivo del usuario. Esto significa: no bloquee la transmisión principal, no "gire" el ventilador de la computadora portátil, no coloque la batería del teléfono. Intercambiamos una mejora en la interactividad de los proyectos web, y el hecho de que su comportamiento se parecía más al comportamiento de las aplicaciones ordinarias, una nueva clase de problemas que no existían en el mundo del renderizado de servidores.



Uno de esos problemas es la pérdida de memoria. Una aplicación de una página mal diseñada puede engullir fácilmente megabytes o incluso gigabytes de memoria. Es capaz de tomar más y más recursos incluso cuando se encuentra en silencio en la pestaña de fondo. La página de dicha aplicación, después de capturar una cantidad exorbitante de recursos, puede comenzar a "ralentizarse" en gran medida. Además, el navegador simplemente puede cerrar la pestaña y decirle al usuario: "Algo salió mal".


Algo salió mal

Por supuesto, los sitios que se representan en el servidor también pueden sufrir un problema de pérdida de memoria. Pero aquí estamos hablando de la memoria del servidor. Al mismo tiempo, es altamente improbable que tales aplicaciones causen una pérdida de memoria en el cliente, ya que el navegador borra la memoria después de cada transición de usuario entre páginas.

El tema de las pérdidas de memoria no está bien cubierto en las publicaciones de desarrollo web. Y a pesar de esto, estoy casi seguro de que la mayoría de las aplicaciones de una sola página no triviales sufren pérdidas de memoria, a menos que los equipos que se ocupan de ellas tengan herramientas confiables para detectar y solucionar este problema. El punto aquí es que en JavaScript es extremadamente fácil asignar aleatoriamente una cierta cantidad de memoria, y luego simplemente olvidar liberar esta memoria.

El autor del artículo, cuya traducción publicamos hoy, compartirá con los lectores su experiencia en la lucha contra las pérdidas de memoria en las aplicaciones web, y también quiere dar ejemplos de su detección efectiva.

¿Por qué se escribe tan poco sobre esto?


Primero, quiero hablar sobre por qué se escribe tan poco sobre las pérdidas de memoria. Supongo que aquí puedes encontrar varias razones:

  • Falta de quejas de los usuarios: la mayoría de los usuarios no están ocupados monitoreando de cerca el administrador de tareas mientras navegan por la web. Por lo general, el desarrollador no encuentra quejas de los usuarios hasta que la pérdida de memoria es tan grave que provoca la incapacidad de trabajar o ralentiza la aplicación.
  • : Chrome - , . .
  • : .
  • : «» . , , , , -.


Las bibliotecas y marcos modernos para desarrollar aplicaciones web, como React, Vue y Svelte, utilizan el modelo de componente de la aplicación. Dentro de este modelo, la forma más común de causar una pérdida de memoria es algo como esto:

window.addEventListener('message', this.onMessage.bind(this));

Eso es todo. Esto es todo lo que se necesita para "equipar" un proyecto con una pérdida de memoria. Para hacer esto, simplemente llame al método addEventListener de algún objeto global (como window, o <body>, o algo similar), y luego, al desmontar el componente, olvide eliminar el detector de eventos utilizando el método removeEventListener .

Pero las consecuencias de esto son aún peores, ya que se produce la fuga de todo el componente. Esto se debe al hecho de que el método está this.onMessageasociado this. Junto con este componente, se produce una fuga de sus componentes secundarios. Es muy probable que todos los nodos DOM asociados con este componente tengan fugas. La situación, como resultado, puede salirse de control muy rápidamente y tener consecuencias muy malas.

Aquí se explica cómo resolver este problema:

//   
this.onMessage = this.onMessage.bind(this);
window.addEventListener('message', this.onMessage);
 
//   
window.removeEventListener('message', this.onMessage);

Situaciones en las que ocurren pérdidas de memoria con mayor frecuencia


La experiencia me dice que las pérdidas de memoria ocurren con mayor frecuencia cuando se usan las siguientes API:

  1. Método addEventListener. Aquí es donde ocurren las pérdidas de memoria con mayor frecuencia. Para resolver el problema, es suficiente llamar en el momento adecuado removeEventListener.
  2. setTimeout setInterval. , (, 30 ), , , , , clearTimeout clearInterval. , setTimeout, «» , , setInterval-. , setTimeout .
  3. API IntersectionObserver, ResizeObserver, MutationObserver . , , . . - , , , , , disconnect . , DOM , , -. -, . — <body>, document, header footer, .
  4. Promise-, , . , , — , . , , «» , . «» .then()-.
  5. Repositorios representados por objetos globales. Cuando usa algo como Redux para controlar el estado de una aplicación , el almacén de estado está representado por un objeto global. Como resultado, si maneja ese almacenamiento descuidadamente, no se eliminarán datos innecesarios, por lo que su tamaño aumentará constantemente.
  6. Infinito crecimiento del DOM. Si la página implementa un desplazamiento sin fin sin el uso de la virtualización , esto significa que el número de nodos DOM en esta página puede crecer de forma ilimitada.

Arriba, examinamos situaciones en las que las pérdidas de memoria ocurren con mayor frecuencia, pero, por supuesto, hay muchos otros casos que nos causan el problema que nos interesa.

Identificación de fuga de memoria


Ahora hemos pasado al desafío de identificar fugas de memoria. Para empezar, no creo que ninguna de las herramientas existentes sea muy adecuada para esto. Probé las herramientas de análisis de memoria de Firefox, probé las herramientas de Edge e IE. Probado incluso Windows Performance Analyzer. Pero las mejores de estas herramientas siguen siendo las Herramientas para desarrolladores de Chrome. Es cierto que en estas herramientas hay muchas "esquinas afiladas" que vale la pena conocer.

Entre las herramientas que ofrece el desarrollador de Chrome, estamos más interesados ​​en el generador Heap snapshotde perfiles de la pestaña Memory, que le permite crear instantáneas de montón. Hay otras herramientas para analizar la memoria en Chrome, pero no he podido extraer beneficios especiales de ellas para detectar pérdidas de memoria.


La herramienta de instantáneas de montón le permite tomar instantáneas de la memoria de la transmisión principal, los trabajadores web o los elementos de iframe.

Si la ventana de la herramienta de Chrome se parece a la que se muestra en la figura anterior, cuando hace clic en el botónTake snapshot, se captura información sobre todos los objetos en la memoria de la máquina virtual seleccionada JavaScript de la página investigada. Esto incluye los objetos a los que se hace referenciawindow, los objetos a los que hacen referencia las devoluciones de llamada utilizadas en la llamadasetInterval, etc. Una instantánea de la memoria se puede percibir como un "momento congelado" del trabajo de la entidad investigada, representando información sobre toda la memoria utilizada por esta entidad.

Después de tomar la fotografía, llegamos al siguiente paso para encontrar fugas. Consiste en reproducir un escenario en el que, según el desarrollador, puede producirse una pérdida de memoria. Por ejemplo, está abriendo y cerrando una determinada ventana modal. Después de cerrar la ventana similar, se espera que la cantidad de memoria asignada vuelva al nivel que existía antes de que se abriera la ventana. Por lo tanto, toman otra foto y luego la comparan con la foto tomada anteriormente. De hecho, la comparación de imágenes es la característica más importante que nos interesa Heap snapshot.


Tomamos la primera instantánea, luego tomamos acciones que pueden causar una pérdida de memoria y luego tomamos otra instantánea. Si no hay fugas, el tamaño de la memoria asignada será igual.

Verdadero, estoHeap snapshotestá lejos de ser una herramienta ideal. Tiene algunas limitaciones que vale la pena conocer:

  1. Incluso si hace clic en el pequeño botón en el panel Memoryque inicia la recolección de basura ( Collect garbage), para asegurarse de que la memoria esté realmente limpia, es posible que deba tomar varias fotos consecutivas. Por lo general tengo tres disparos. Aquí vale la pena centrarse en el tamaño total de cada imagen; al final, debería estabilizarse.
  2. -, -, iframe, , , . , JavaScript. — , , .
  3. «». .

En este punto, si su aplicación es bastante compleja, es posible que observe muchos objetos "con fugas" al comparar instantáneas. Aquí la situación es algo complicada, ya que lo que puede confundirse con una pérdida de memoria no siempre es el caso. Gran parte de lo sospechoso son procesos normales para trabajar con objetos. La memoria ocupada por algunos objetos se borra para colocar otros objetos en esta memoria, algo se vacía en la memoria caché y la memoria correspondiente no se borra de inmediato, y así sucesivamente.

Nos abrimos paso a través del ruido de la información


Descubrí que la mejor manera de romper el ruido de la información es repetir las acciones que se supone que causan una pérdida de memoria. Por ejemplo, en lugar de abrir y cerrar la ventana modal solo una vez después de capturar el primer disparo, esto se puede hacer 7 veces. ¿Por qué 7? Sí, aunque solo sea porque 7 es un primo notable. Luego, debe realizar un segundo disparo y, comparándolo con el primero, averiguar si cierto objeto "se filtró" 7 veces (o 14 veces, o 21 veces).


Compare las instantáneas de montón. Tenga en cuenta que estamos comparando la imagen n. ° 3 con la imagen n. ° 6. El hecho es que tomé tres fotos seguidas para que Chrome tuviera más sesiones de recolección de basura. Además, tenga en cuenta que algunos objetos "se filtraron" 7 veces.

Otro truco útil es que, al comienzo del estudio, antes de crear la primera imagen, realice el procedimiento una vez, durante el cual, como se esperaba, pérdida de memoria. Esto se recomienda especialmente si se utiliza la división de código en el proyecto. En tal caso, es muy probable que tras la primera ejecución de la acción sospechosa, se carguen los módulos JavaScript necesarios, lo que afectará la cantidad de memoria asignada.

Ahora puede tener una pregunta sobre por qué debe prestar especial atención a la cantidad de objetos y no a la cantidad total de memoria. Aquí podemos decir que nos esforzamos intuitivamente por reducir la cantidad de "pérdida" de memoria. En este sentido, puede pensar que debe controlar la cantidad total de memoria utilizada. Pero este enfoque, por una razón importante, no nos conviene particularmente bien.

Si algo "se escapa", sucede porque (volviendo a contar a Joe Armstrong ) necesitas un plátano, pero terminas con un plátano, el gorila que lo sostiene y, además, toda la jungla. Si nos centramos en la cantidad total de memoria, será lo mismo que "medir" la jungla, y no el plátano que nos interesa.


Gorila comiendo un plátano.

Ahora volvamos al ejemplo anterior conaddEventListener. Una fuente de fuga es un detector de eventos que hace referencia a una función. Y esta función, a su vez, se refiere a un componente que, posiblemente, almacena enlaces a un montón de cosas buenas como matrices, cadenas y objetos.

Si analiza la diferencia entre las imágenes, ordenando las entidades por la cantidad de memoria que ocupan, esto le permitirá ver muchas matrices, líneas, objetos, la mayoría de los cuales probablemente no estén relacionados con la fuga. Y después de todo, necesitamos encontrar el oyente del evento desde el cual todo comenzó. Él, en comparación con lo que se refiere, ocupa muy poca memoria. Para arreglar la fuga, necesitas encontrar un plátano, no la jungla.

Como resultado, si ordena los registros por el número de objetos "filtrados", notará 7 oyentes de eventos. Y tal vez 7 componentes y 14 subcomponentes, y tal vez algo más como eso. Este número 7 debe sobresalir del panorama general, ya que es, sin embargo, un número bastante notable e inusual. En este caso, no importa cuántas veces se repita la acción sospechosa. Cuando se examinan imágenes, si las sospechas están justificadas, se registrarán tantos objetos "filtrados". Así es como puede identificar rápidamente la fuente de una pérdida de memoria.

Análisis de árbol de enlaces


La herramienta para crear instantáneas proporciona la capacidad de ver "cadenas de enlaces" que lo ayudan a descubrir a qué objetos hacen referencia otros objetos. Esto es lo que permite que la aplicación funcione. Al analizar tales "cadenas" o "árboles" de enlaces, puede averiguar exactamente dónde se asignó la memoria para el objeto "con fugas".


La cadena de enlaces le permite averiguar qué objeto se refiere al objeto "con fugas". Al leer estas cadenas, es necesario tener en cuenta que los objetos ubicados en ellas a continuación se refieren a los objetos ubicados arriba.

En el ejemplo anterior, hay una variable llamadasomeObjectreferenciada en el cierre (context) al que hace referencia el detector de eventos. Si hace clic en el enlace que conduce al código fuente, se mostrará un texto bastante comprensible del programa:

class SomeObject () { /* ... */ }
 
const someObject = new SomeObject();
const onMessage = () => { /* ... */ };
window.addEventListener('message', onMessage);

Si comparamos este código con la figura anterior, resulta que contextla figura es un cierre al onMessageque se refiere someObject. Este es un ejemplo artificial . Las pérdidas reales de memoria pueden ser mucho menos obvias.

Vale la pena señalar que la herramienta de instantánea del montón tiene algunas limitaciones:

  1. Si guarda un archivo de instantánea y luego lo carga de nuevo, se pierden los enlaces a archivos con código. Es decir, después de haber descargado una instantánea, no será posible descubrir que el código de cierre del detector de eventos está en la línea 22 del archivo foo.js. Dado que esta información es extremadamente importante, guardar archivos de instantáneas de almacenamiento dinámico o, por ejemplo, transferirlos a alguien, es casi inútil.
  2. WeakMap, Chrome , . , , , , . WeakMap — .
  3. Chrome , . , , , , . , object, EventListener. object — , , , «» 7 .

Esta es una descripción de mi estrategia básica para identificar pérdidas de memoria. He utilizado con éxito esta técnica para detectar docenas de fugas.

Es cierto que debo decir que esta guía para encontrar pérdidas de memoria cubre solo una pequeña parte de lo que sucede en la realidad. Esto es solo el comienzo del trabajo. Además, debe poder manejar la instalación de puntos de interrupción, el registro y las correcciones de prueba para determinar si resuelven el problema. Y, desafortunadamente, todo esto, en esencia, se traduce en una seria inversión de tiempo.

Análisis automatizado de fugas de memoria


Quiero comenzar esta sección con el hecho de que no pude encontrar un buen enfoque para automatizar la detección de pérdidas de memoria. Chrome tiene su propia API performance.memory , pero por razones de privacidad, no le permite recopilar datos suficientemente detallados. Como resultado, esta API no se puede usar en producción para detectar fugas. El Grupo de trabajo de rendimiento web del W3C discutió anteriormente las herramientas de memoria, pero sus miembros aún no han acordado un nuevo estándar diseñado para reemplazar esta API.

En entornos de prueba, puede aumentar la granularidad de la salida de datos performance.memoryutilizando el indicador de Chrome --enable-precision-memory-info. Las instantáneas de montón todavía se pueden crear utilizando el propio equipo de Chromedriver: takeHeapSnapshot . Este equipo tiene las mismas limitaciones que ya hemos discutido. Es probable que si usa este comando, entonces, por las razones descritas anteriormente, tenga sentido llamarlo tres veces, y luego tomar solo lo que recibió como resultado de su última llamada.

Como los oyentes de eventos son la fuente más común de pérdidas de memoria, hablaré sobre otra técnica de detección de pérdidas que uso. Consiste en crear parches de mono para la API addEventListenery removeEventListeneren contar los enlaces para verificar que su número vuelva a cero. Aquí hay un ejemplo de cómo se hace esto.

En las Herramientas para desarrolladores de Chrome, también puede usar la API nativa getEventListeners para averiguar qué escuchas de eventos están conectados a un elemento en particular. Sin embargo, este comando solo está disponible en la barra de herramientas del desarrollador.

Quiero agregar que Matthias Binens me habló de otra API útil de herramientas de Chrome. Estos son queryObjects . Con él, puede obtener información sobre todos los objetos creados con un determinado constructor. Aquí hay un buen material sobre este tema sobre la automatización de la detección de pérdidas de memoria en Puppeteer.

Resumen


La búsqueda y reparación de pérdidas de memoria en aplicaciones web todavía está en pañales. Aquí hablé sobre algunas técnicas que, en mi caso, funcionaron bien. Pero debe reconocerse que la aplicación de estas técnicas todavía está llena de ciertas dificultades y consume mucho tiempo.

Como con cualquier problema de rendimiento, como dicen, una pizca por adelantado vale una libra. Quizás alguien encuentre útil preparar las pruebas sintéticas apropiadas en lugar de analizar la fuga después de que ya ha ocurrido. Y si no se trata de una fuga, sino de varias, entonces el análisis del problema puede convertirse en algo así como pelar cebollas: después de que se soluciona un problema, se descubre otro y luego este proceso se repite (y todo este tiempo, como las cebollas). , lágrimas en los ojos). Las revisiones de código también pueden ayudar a identificar patrones de fuga comunes. Pero esto, si sabes, dónde buscar.

JavaScript es un lenguaje que proporciona un trabajo seguro con memoria. Por lo tanto, existe cierta ironía en la facilidad con que se producen pérdidas de memoria en las aplicaciones web. Es cierto que esto se debe en parte a las características de las interfaces de usuario del dispositivo. Necesita escuchar muchos eventos: eventos de mouse, eventos de desplazamiento, eventos de teclado. La aplicación de todos estos patrones puede conducir fácilmente a pérdidas de memoria. Pero, esforzándonos por garantizar que nuestras aplicaciones web utilicen la memoria con moderación, podemos aumentar su rendimiento y protegerlas de "bloqueos". Además, demostramos respeto por los límites de recursos de los dispositivos de los usuarios.

¡Queridos lectores! ¿Ha encontrado pérdidas de memoria en sus proyectos web?


All Articles