Combate a vazamentos de memória em aplicativos da web

Quando passamos do desenvolvimento de sites cujas páginas são formadas no servidor para a criação de aplicativos Web de página única que são renderizados no cliente, adotamos certas regras do jogo. Um deles é o manuseio preciso dos recursos no dispositivo do usuário. Isso significa - não bloqueie o fluxo principal, não "gire" o ventilador do laptop, não coloque a bateria do telefone. Trocamos uma melhoria na interatividade dos projetos da Web e o fato de o comportamento deles se parecer mais com o de aplicativos comuns, uma nova classe de problemas que não existia no mundo da renderização de servidores.



Um desses problemas são vazamentos de memória. Um aplicativo de uma página mal projetado pode consumir facilmente megabytes ou mesmo gigabytes de memória. É capaz de ocupar cada vez mais recursos, mesmo quando está silenciosamente na guia plano de fundo. A página desse aplicativo, depois de capturar uma quantidade exorbitante de recursos, pode começar a "desacelerar" bastante. Além disso, o navegador pode simplesmente fechar a guia e informar ao usuário: "Algo deu errado".


Algo deu errado

Obviamente, os sites renderizados no servidor também podem sofrer um problema de vazamento de memória. Mas aqui estamos falando sobre memória do servidor. Ao mesmo tempo, é altamente improvável que esses aplicativos causem um vazamento de memória no cliente, pois o navegador limpa a memória após cada transição de usuário entre as páginas.

O tópico vazamentos de memória não é bem abordado nas publicações de desenvolvimento da web. E, apesar disso, tenho quase certeza de que a maioria dos aplicativos não triviais de página única sofre com vazamentos de memória - a menos que as equipes que lidam com eles tenham ferramentas confiáveis ​​para detectar e corrigir esse problema. O ponto aqui é que, em JavaScript, é extremamente fácil alocar aleatoriamente uma certa quantidade de memória e, em seguida, esqueça de liberar essa memória.

O autor do artigo, cuja tradução estamos publicando hoje, compartilhará com os leitores sua experiência no combate a vazamentos de memória em aplicativos da Web e também deseja dar exemplos de sua detecção efetiva.

Por que tão pouco está escrito sobre isso?


Primeiro, quero falar sobre por que tão pouco está escrito sobre vazamentos de memória. Suponho que aqui você pode encontrar vários motivos:

  • Falta de reclamações dos usuários: a maioria dos usuários não está ocupada monitorando de perto o gerenciador de tarefas enquanto navega na web. Normalmente, o desenvolvedor não encontra reclamações de usuários até que o vazamento de memória seja tão grave que faça com que a incapacidade funcione ou diminua a velocidade do aplicativo.
  • : Chrome - , . .
  • : .
  • : «» . , , , , -.


Bibliotecas e estruturas modernas para o desenvolvimento de aplicativos da Web, como React, Vue e Svelte, usam o modelo de componente do aplicativo. Nesse modelo, a maneira mais comum de causar vazamento de memória é algo como isto:

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

Isso é tudo. Isso é tudo o que é necessário para "equipar" um projeto com um vazamento de memória. Para fazer isso, basta chamar o método addEventListener de algum objeto global (como window, ou <body>algo semelhante) e, ao desmontar o componente, esqueça de remover o ouvinte de eventos usando o método removeEventListener .

Mas as consequências disso são ainda piores, pois ocorre o vazamento de todo o componente. Isso se deve ao fato de o método estar this.onMessageanexado this. Junto com esse componente, ocorre o vazamento de seus componentes filhos. É muito provável que todos os nós do DOM associados a este componente vazem. A situação, como resultado, pode sair do controle muito rapidamente, levando a consequências muito ruins.

Veja como resolver esse problema:

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

Situações em que vazamentos de memória ocorrem com mais frequência


A experiência me diz que o vazamento de memória ocorre com mais frequência ao usar as seguintes APIs:

  1. Método addEventListener. É aqui que os vazamentos de memória ocorrem com mais frequência. Para resolver o problema, basta ligar na hora certa 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. Repositórios representados por objetos globais. Quando você usa algo como Redux para controlar o estado de um aplicativo , o armazenamento de estado é representado por um objeto global. Como resultado, se você lidar com esse armazenamento de forma descuidada, dados desnecessários não serão excluídos dele, como resultado do qual seu tamanho aumentará constantemente.
  6. Crescimento infinito do DOM. Se a página implementar rolagem sem fim, sem o uso da virtualização , isso significa que o número de nós DOM nesta página poderá aumentar ilimitadamente.

Acima, examinamos situações nas quais vazamentos de memória ocorrem com mais frequência, mas, é claro, existem muitos outros casos que causam o problema de nosso interesse.

Identificação de vazamento de memória


Agora passamos ao desafio de identificar vazamentos de memória. Para começar, não acho que nenhuma das ferramentas existentes seja muito adequada para isso. Eu tentei as ferramentas de análise de memória do Firefox, tentei as ferramentas do Edge e do IE. Testou até o Windows Performance Analyzer. Mas as melhores ferramentas ainda são as Ferramentas do desenvolvedor do Chrome. É verdade que nessas ferramentas existem muitos "cantos afiados" que valem a pena conhecer.

Entre as ferramentas que o desenvolvedor do Chrome fornece, estamos mais interessados Heap snapshotno criador de perfil de guias Memory, que permite criar instantâneos de heap. Existem outras ferramentas para analisar a memória no Chrome, mas não consegui extrair benefícios especiais delas na detecção de vazamentos de memória.


A ferramenta Haps snapshot permite tirar instantâneos da memória do fluxo principal, trabalhadores da Web ou elementos iframe.

Se a janela da ferramenta Chrome se parecer com a mostrada na figura anterior, quando você clicar no botãoTake snapshot, serão capturadas informações sobre todos os objetos na memória da máquina virtual selecionada JavaScript da página investigada. Isso inclui objetos mencionados emwindow, objetos referenciados pelos retornos de chamada usados ​​na chamadasetIntervale assim por diante. Um instantâneo da memória pode ser percebido como um "momento congelado" do trabalho da entidade investigada, representando informações sobre toda a memória usada por essa entidade.

Depois que a foto é tirada, chegamos ao próximo passo para encontrar vazamentos. Consiste em reproduzir um cenário em que, de acordo com o desenvolvedor, pode ocorrer um vazamento de memória. Por exemplo, está abrindo e fechando uma determinada janela modal. Depois que a janela semelhante é fechada, espera-se que a quantidade de memória alocada retorne ao nível que existia antes da abertura da janela. Portanto, eles tiram outra foto e a comparam com a foto tirada anteriormente. De fato, a comparação de imagens é a característica mais importante de nosso interesse Heap snapshot.


Tiramos o primeiro instantâneo, depois executamos ações que podem causar um vazamento de memória e depois outro. Se não houver vazamento, o tamanho da memória alocada será igual

, pois issoHeap snapshotestá longe de ser uma ferramenta ideal. Vale a pena conhecer algumas limitações:

  1. Mesmo se você clicar no pequeno botão no painel Memoryque inicia a coleta de lixo ( Collect garbage), para ter certeza de que a memória está realmente limpa, talvez seja necessário tirar várias fotos consecutivas. Eu normalmente tenho três doses. Aqui vale a pena focar no tamanho total de cada imagem - ela, no final, deve estabilizar.
  2. -, -, iframe, , , . , JavaScript. — , , .
  3. «». .

Nesse ponto, se o seu aplicativo for bastante complexo, você poderá observar muitos objetos "vazando" ao comparar instantâneos. Aqui a situação é um pouco complicada, pois o que pode ser confundido com um vazamento de memória nem sempre é o caso. Muito do que é suspeito é apenas processos normais para trabalhar com objetos. A memória ocupada por alguns objetos é limpa para colocar outros objetos nessa memória, algo é liberado no cache e, portanto, a memória correspondente não é limpa imediatamente e assim por diante.

Abrimos caminho através do ruído da informação


Eu descobri que a melhor maneira de romper o ruído da informação é repetir as ações que deveriam causar um vazamento de memória. Por exemplo, em vez de abrir e fechar a janela modal apenas uma vez após a captura da primeira foto, isso pode ser feito 7 vezes. Por que 7? Sim, apenas porque 7 é um primo perceptível. Então você precisa fazer uma segunda tentativa e, comparando com a primeira, descobrir se um determinado objeto “vazou” 7 vezes (ou 14 vezes ou 21 vezes).


Compare instantâneos de heap. Observe que estamos comparando a imagem nº 3 com a imagem nº 6. O fato é que tirei três fotos seguidas para que o Chrome tivesse mais sessões de coleta de lixo. Além disso, observe que alguns objetos “vazaram” 7 vezes.

Outro truque útil é que, no início do estudo, antes de criar a primeira imagem, execute o procedimento uma vez, durante o qual, conforme o esperado, vazamento de memória. Isso é especialmente recomendado se a divisão de código for usada no projeto. Nesse caso, é muito provável que, após a primeira execução da ação suspeita, os módulos JavaScript necessários sejam carregados, o que afetará a quantidade de memória alocada.

Agora você pode ter uma pergunta sobre por que deve prestar atenção especial ao número de objetos e não à quantidade total de memória. Aqui podemos dizer que nos esforçamos intuitivamente para reduzir a quantidade de memória "vazando". Nesse sentido, você pode pensar que deve monitorar a quantidade total de memória usada. Mas essa abordagem, por um motivo importante, não nos convém particularmente bem.

Se algo “vaza”, acontece porque (recontando Joe Armstrong ) você precisa de uma banana, mas acaba com uma banana, o gorila que a segura e, além disso, toda a selva. Se focarmos na quantidade total de memória, será o mesmo que “medir” a floresta, e não a banana que nos interessa.


Gorila comendo uma banana

Agora, de volta ao exemplo acima comaddEventListener. Uma fonte de vazamento é um ouvinte de eventos que faz referência a uma função. E essa função, por sua vez, refere-se a um componente que, possivelmente, armazena links para várias coisas boas, como matrizes, seqüências de caracteres e objetos.

Se você analisar a diferença entre as imagens, classificando as entidades pela quantidade de memória que elas ocupam, isso permitirá que você veja muitas matrizes, linhas, objetos, a maioria dos quais provavelmente não está relacionada ao vazamento. E, afinal, precisamos encontrar o próprio ouvinte de eventos a partir do qual tudo começou. Ele, em comparação com o que ele se refere, ocupa muito pouca memória. Para consertar o vazamento, você precisa encontrar uma banana, não a selva.

Como resultado, se você classificar os registros pelo número de objetos "vazados", notará 7 ouvintes de eventos. E talvez 7 componentes e 14 subcomponentes, e talvez algo assim. Este número 7 deve se destacar do quadro geral, pois é, no entanto, um número bastante perceptível e incomum. Nesse caso, não importa quantas vezes a ação suspeita seja repetida. Ao examinar imagens, se as suspeitas forem justificadas, elas serão gravadas com o mesmo número de objetos "vazados". É assim que você pode identificar rapidamente a fonte de um vazamento de memória.

Análise de Árvore de Link


A ferramenta para criar capturas instantâneas fornece a capacidade de visualizar "cadeias de links" que ajudam a descobrir quais objetos são referenciados por outros objetos. É isso que permite que o aplicativo funcione. Analisando essas "cadeias" ou "árvores" de links, você pode descobrir exatamente onde a memória foi alocada para o objeto "vazando".


A cadeia de links permite descobrir qual objeto se refere ao objeto "com vazamento". Ao ler essas cadeias, é necessário levar em consideração que os objetos localizados nelas abaixo se referem aos objetos localizados

acima.No exemplo acima, há uma variável chamadasomeObjectreferenciada no encerramento (context) referenciado pelo ouvinte de evento. Se você clicar no link que leva ao código fonte, será exibido um texto bastante compreensível do programa:

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

Se compararmos esse código com a figura anterior, verifica-se que contexta figura é um fechamento a onMessageque se refere someObject. Este é um exemplo artificial . Vazamentos reais de memória podem ser muito menos óbvios.

Vale ressaltar que a ferramenta de instantâneo de heap tem algumas limitações:

  1. Se você salvar um arquivo de captura instantânea e enviá-lo novamente, os links para arquivos com código serão perdidos. Ou seja, por exemplo, após o download de um instantâneo, não será possível descobrir que o código de fechamento do ouvinte de eventos está na linha 22 do arquivo foo.js. Como essas informações são extremamente importantes, salvar arquivos de instantâneo de heap ou, por exemplo, transferi-los para alguém, é quase inútil.
  2. WeakMap, Chrome , . , , , , . WeakMap — .
  3. Chrome , . , , , , . , object, EventListener. object — , , , «» 7 .

Esta é uma descrição da minha estratégia básica para identificar vazamentos de memória. Utilizei com sucesso essa técnica para detectar dezenas de vazamentos.

É verdade que devo dizer que este guia para encontrar vazamentos de memória cobre apenas uma pequena parte do que está acontecendo na realidade. Este é apenas o começo do trabalho. Além disso, você precisa ser capaz de lidar com a instalação de pontos de interrupção, registro em log e correções de teste para determinar se eles resolvem o problema. E, infelizmente, tudo isso, em essência, se traduz em um sério investimento de tempo.

Análise automatizada de vazamento de memória


Desejo iniciar esta seção com o fato de não encontrar uma boa abordagem para automatizar a detecção de vazamentos de memória. O Chrome possui sua própria API performance.memory , mas por motivos de privacidade, não permite coletar dados suficientemente detalhados. Como resultado, essa API não pode ser usada na produção para detectar vazamentos. O W3C Web Performance Working Group discutiu anteriormente as ferramentas de memória, mas seus membros ainda não concordaram com um novo padrão projetado para substituir esta API.

Em ambientes de teste, você pode aumentar a granularidade da saída de dados performance.memoryusando o sinalizador do Chrome --enable-specific-memory-info. Os instantâneos de heap ainda podem ser criados usando a própria equipe do Chromedriver: takeHeapSnapshot . Essa equipe tem as mesmas limitações que já discutimos. É provável que, se você usar esse comando, pelos motivos descritos acima, faça sentido chamá-lo três vezes e, em seguida, leve apenas o que foi recebido como resultado da última chamada.

Como os ouvintes de eventos são a fonte mais comum de vazamento de memória, falarei sobre outra técnica de detecção de vazamento que uso. Consiste na criação de patches de macaco para a API addEventListenere removeEventListenerna contagem dos links para verificar se o número deles está retornando a zero. Aqui está um exemplo de como isso é feito.

Nas Ferramentas do desenvolvedor do Chrome, você também pode usar a API nativa getEventListeners para descobrir quais ouvintes de eventos estão anexados a um elemento específico. Este comando, no entanto, está disponível apenas na barra de ferramentas do desenvolvedor.

Quero acrescentar que Matthias Binens me falou sobre outra API útil de ferramentas do Chrome. Estes são queryObjects . Com ele, você pode obter informações sobre todos os objetos criados usando um determinado construtor. Aqui está um bom material sobre este tópico sobre como automatizar a detecção de vazamento de memória no Puppeteer.

Sumário


A pesquisa e a correção de vazamentos de memória em aplicativos da web ainda estão engatinhando. Aqui eu falei sobre algumas técnicas que, no meu caso, tiveram um bom desempenho. Mas deve-se reconhecer que a aplicação dessas técnicas ainda está repleta de certas dificuldades e consome muito tempo.

Como em qualquer problema de desempenho, como se costuma dizer, uma pitada de antecedência vale um quilo. Talvez alguém ache útil preparar os testes sintéticos apropriados, em vez de analisar o vazamento depois que ele já ocorreu. E se não é um vazamento, mas vários, a análise do problema pode se transformar em algo como descascar cebolas: depois que um problema é corrigido, outro é descoberto e esse processo se repete (e todo esse tempo, como nas cebolas) lágrimas nos olhos). As revisões de código também podem ajudar a identificar padrões comuns de vazamento. Mas isso - se você souber - para onde olhar.

JavaScript é uma linguagem que fornece trabalho seguro com memória. Portanto, há alguma ironia na facilidade com que vazamentos de memória acontecem em aplicativos da web. É verdade que isso se deve em parte aos recursos das interfaces de usuário do dispositivo. Você precisa ouvir muitos eventos: eventos do mouse, eventos de rolagem, eventos do teclado. A aplicação de todos esses padrões pode facilmente levar a vazamentos de memória. Mas, nos esforçando para garantir que nossos aplicativos da Web usem a memória com moderação, podemos aumentar seu desempenho e protegê-los de "falhas". Além disso, demonstramos respeito pelos limites de recursos dos dispositivos do usuário.

Queridos leitores! Você encontrou vazamentos de memória em seus projetos da web?


All Articles