Como o JavaScript funciona: gerenciamento de memória + como lidar com 4 vazamentos comuns de memória

Robisson Oliveira
React Brasil
19 min readFeb 13, 2019

--

Algumas semanas atrás, começamos uma série destinada a aprofundar o JavaScript e como ele realmente funciona: pensamos que conhecendo os blocos de construção do JavaScript e como eles funcionam juntos, você poderá escrever códigos e aplicativos melhores.

Abaixo estão os posts dessa série publicados até o momento

O primeiro post da série se concentrou em fornecer uma visão geral da engine, do tempo de execução e da call stack . O segundo post examinou de perto as partes internas da engine JavaScript V8 do Google e também forneceu algumas dicas sobre como escrever um código JavaScript melhor.

Neste terceiro post, discutiremos outro tópico crítico que está ficando cada vez mais negligenciado pelos desenvolvedores devido à crescente maturidade e complexidade das linguagens de programação que estão sendo usadas diariamente — gerenciamento de memória. Também forneceremos algumas dicas sobre como lidar com vazamentos de memória em JavaScript que seguimos no SessionStack, já que precisamos garantir que o SessionStack não cause vazamentos de memória ou que não aumente o consumo de memória do aplicativo da Web no qual estamos integrados.

Visão geral

Linguagens como como C, possuem gerenciamento de memória primitivos de baixo nível, como malloc() e free(). Esses primitivos são usadas pelo desenvolvedor para alocar e liberar explicitamente a memória de e para o sistema operacional.

Ao mesmo tempo, JavaScript aloca memória quando coisas (objetos, strings, etc.) são criadas e “automaticamente” a libera quando elas não são mais usadas, um processo chamado de garbage collection (coleta de lixo) . Essa natureza aparentemente “automática” de liberar recursos é uma fonte de confusão e dá aos desenvolvedores de JavaScript (e de outros idiomas de alto nível) a falsa impressão de que eles podem optar por não se importar com o gerenciamento de memória. Isso é um grande erro.

Mesmo quando se trabalha com linguagens de alto nível, os desenvolvedores devem ter uma compreensão do gerenciamento de memória (ou pelo menos o básico). Às vezes, há problemas com o gerenciamento automático de memória (como erros ou limitações de implementação nos coletores de lixo, etc.) que os desenvolvedores precisam entender para manipulá-los adequadamente (ou para encontrar uma solução adequada, com uma troca mínima e código dívida).

Ciclo de vida da memória

Não importa qual linguagem de programação você esteja usando, o ciclo de vida da memória é praticamente sempre o mesmo:

Aqui está uma visão geral do que acontece em cada etapa do ciclo:

  • Allocate memory (Alocar memória) — A memória é alocada pelo sistema operacional, o que permite ao seu programa usá-la. Em linguagens de baixo nível (por exemplo, C) esta é uma operação explícita que você, como desenvolvedor, deve manipular. Em linguagens de alto nível, no entanto, isso é feito para você.
  • Use memory (Use memória) — este é o momento em que seu programa realmente faz uso da memória alocada anteriormente. As operações de leitura e gravação estão ocorrendo enquanto você está usando as variáveis ​​alocadas em seu código.
  • Release memory (Liberar memória) — agora é a hora de liberar toda a memória de que você não precisa para que ela fique livre e disponível novamente. Tal como acontece com a operação de Allocate memory, esta é explícita em linguagens de baixo nível.

Para uma rápida visão geral dos conceitos da call stack e do heap da memória, você pode ler nosso primeiro post sobre o tópico .

O que é memória?

Antes de saltar direto para a memória em JavaScript, discutiremos brevemente o que é memória em geral e como ela funciona em poucas palavras.

Em um nível de hardware, a memória do computador consiste em um grande número de interruptores (flip-flop). Cada flip-flop contém alguns transistores e é capaz de armazenar um bit. Os flip-flops individuais são endereçáveis ​​por um identificador único , para que possamos ler e sobrescrevê-los. Assim, conceitualmente, podemos pensar em toda a nossa memória de computador como um conjunto gigantesco de bits que podemos ler e escrever.

Uma vez que, como seres humanos, não somos tão bons em fazer todo o nosso pensamento e aritmética em bits, nós os organizamos em grupos maiores, que juntos podem ser usados ​​para representar números. 8 bits são chamados 1 byte. Além dos bytes, existem palavras (às vezes 16, às vezes 32 bits).

Muitas coisas estão armazenadas nesta memória:

  1. Todas as variáveis ​​e outros dados usados ​​por todos os programas.
  2. O código dos programas, incluindo o sistema operacional.

O compilador e o sistema operacional trabalham juntos para cuidar da maior parte do gerenciamento de memória para você, mas recomendamos que você dê uma olhada no que está acontecendo sob o capô.

Quando você compila seu código, o compilador pode examinar os tipos de dados primitivos e calcular antecipadamente a quantidade de memória necessária. A quantidade necessária é então alocada para o programa no espaço da call stack. O espaço em que essas variáveis ​​são alocadas é chamado de espaço de pilha, porque, conforme as funções são chamadas, sua memória é incluída na parte superior da memória existente. Quando eles terminam, eles são removidos em uma ordem LIFO (last-in, first-out). Por exemplo, considere as seguintes declarações:

int n; // 4 bytes
int x[4]; // array of 4 elements, each 4 bytes
double m; // 8 bytes

O compilador pode ver imediatamente que o código requer
4 + 4 × 4 + 8 = 28 bytes.

É assim que funciona com os tamanhos atuais para integer e doubles. Cerca de 20 anos atrás, os números inteiros eram tipicamente de 2 bytes e de 4 bytes duplos. Seu código nunca deve depender do que é neste momento o tamanho dos tipos básicos de dados.

O compilador vai inserir código que irá interagir com o sistema operacional para solicitar o número necessário de bytes na stack para que suas variáveis ​​sejam armazenadas.

No exemplo acima, o compilador conhece o endereço de memória exato de cada variável. De fato, sempre que escrevemos para a variável n , isso é traduzido em algo como “endereço de memória 4127963” internamente.

Observe que, se tentássemos acessar x[4] aqui, teríamos acessado os dados associados a m. Isso porque estamos acessando um elemento na matriz que não existe - são 4 bytes a mais do que o último elemento real alocado na matriz, que é x[3] , e pode acabar lendo (ou sobrescrevendo) alguns dos m ' s bits. Isto quase certamente teria consequências muito indesejadas para o resto do programa.

Quando as funções chamam outras funções, cada uma recebe seu próprio pedaço da stack quando é chamada. Ela mantém todas as suas variáveis ​​locais lá, mas também um contador de programa que lembra onde estava sua execução. Quando a função termina, seu bloco de memória é novamente disponibilizado para outros propósitos.

Alocação dinâmica

Infelizmente, as coisas não são tão fáceis quando não sabemos em tempo de compilação quanto de memória uma variável precisará. Suponha que queremos fazer algo como o seguinte:

int n = readInput(); // reads input from the user...// create an array with "n" elements

Aqui, em tempo de compilação, o compilador não sabe quanta memória o array precisará porque é determinado pelo valor fornecido pelo usuário.

Portanto, não é possível alocar espaço para uma variável na pilha. Em vez disso, nosso programa precisa solicitar explicitamente ao sistema operacional a quantidade certa de espaço em tempo de execução. Essa memória é atribuída do espaço de heap . A diferença entre alocação de memória estática e dinâmica é resumida na tabela a seguir:

Diferenças entre memória alocada estaticamente e dinamicamente

Para entender completamente como funciona a alocação de memória dinâmica, precisamos gastar mais tempo em ponteiros, o que pode ser um desvio muito grande do tópico deste post. Se você estiver interessado em aprender mais, deixe-me saber nos comentários e podemos entrar em mais detalhes sobre os ponteiros em um post futuro.

Alocação em JavaScript

Agora vamos explicar como o primeiro passo (alocar memória ) funciona em JavaScript.

O JavaScript alivia os desenvolvedores da responsabilidade de lidar com alocações de memória — o JavaScript faz isso sozinho, além de declarar valores.

var n = 374; // allocates memory for a number
var s = 'sessionstack'; // allocates memory for a string
var o = {
a: 1,
b: null
}; // allocates memory for an object and its contained values
var a = [1, null, 'str']; // (like object) allocates memory for the
// array and its contained values
function f(a) {
return a + 3;
} // allocates a function (which is a callable object)
// function expressions also allocate an object
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'blue';
}, false);

Algumas chamadas de função também resultam na alocação de objetos:

var d = new Date(); // allocates a Date objectvar e = document.createElement('div'); // allocates a DOM element

Os métodos podem alocar novos valores ou objetos:

var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 is a new string
// Since strings are immutable,
// JavaScript may decide to not allocate memory,
// but just store the [0, 3] range.
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2);
// new array with 4 elements being
// the concatenation of a1 and a2 elements

Usando memória em JavaScript

Usando a memória alocada em JavaScript, basicamente, significa ler e escrever nela.

Isso pode ser feito lendo ou escrevendo o valor de uma variável ou uma propriedade de objeto ou até mesmo passando um argumento para uma função.

Libere quando a memória não for mais necessária

A maioria dos problemas de gerenciamento de memória ocorre nesse estágio.

A tarefa mais difícil aqui é descobrir quando a memória alocada não é mais necessária. Geralmente requer que o desenvolvedor determine onde no programa essa parte da memória não é mais necessária e liberte-a.

As linguagens de alto nível incorporam um software chamado garbage collector (coletor de lixo), cujo trabalho é rastrear a alocação de memória e usá-la para descobrir quando uma parte da memória alocada não é mais necessária. Nesse caso, ela será automaticamente liberada.

Infelizmente, esse processo é uma aproximação, já que o problema geral de saber se alguma parte da memória é necessária é indecidível (não pode ser resolvido por um algoritmo).

A maioria dos coletores de lixo trabalha coletando memória que não pode mais ser acessada, por exemplo, todas as variáveis ​​que apontam para ela ficaram fora do escopo. Isso é, no entanto, uma sub-aproximação do conjunto de espaços de memória que podem ser coletados, porque a qualquer momento um local de memória ainda pode ter uma variável apontando para ele no escopo, mas nunca será acessado novamente.

Garbage collection (Coleta de lixo)

Devido ao fato de que descobrir se alguma memória “não é mais necessária” é indecidível, as coletas de lixo implementam uma restrição de uma solução para o problema geral. Esta seção explicará as noções necessárias para entender os principais algoritmos de coleta de lixo e suas limitações.

Memory references (Referências de memória)

O conceito principal de algoritmos de coleta de lixo é o de referência .

No contexto do gerenciamento de memória, um objeto é chamado de referência a outro objeto se o primeiro tiver acesso ao último (pode ser implícito ou explícito). Por exemplo, um objeto JavaScript tem uma referência ao seu protótipo ( referência implícita ) e aos valores de suas propriedades ( referência explícita ).

Nesse contexto, a ideia de um “objeto” é estendida para algo mais amplo do que objetos JavaScript regulares e também contém escopos de função (ou o escopo léxico global).

O escopo léxico define como os nomes de variáveis ​​são resolvidos em funções aninhadas: funções internas contêm o escopo de funções pai mesmo se a função pai tiver retornado.

Coleta de lixo de contagem de referência

Esse é o algoritmo de coleta de lixo mais simples. Um objeto é considerado “lixo colecionável” se houver referências zero apontando para ele.

Dê uma olhada no seguinte código:

var o1 = {
o2: {
x: 1
}
};
// 2 objects are created.
// 'o2' is referenced by 'o1' object as one of its properties.
// None can be garbage-collected
var o3 = o1; // the 'o3' variable is the second thing that
// has a reference to the object pointed by 'o1'.
o1 = 1; // now, the object that was originally in 'o1' has a
// single reference, embodied by the 'o3' variable

var o4 = o3.o2; // reference to 'o2' property of the object.
// This object has now 2 references: one as
// a property.
// The other as the 'o4' variable

o3 = '374'; // The object that was originally in 'o1' has now zero
// references to it.
// It can be garbage-collected.
// However, what was its 'o2' property is still
// referenced by the 'o4' variable, so it cannot be
// freed.

o4 = null; // what was the 'o2' property of the object originally in
// 'o1' has zero references to it.
// It can be garbage collected.

Os ciclos estão criando problemas

Há uma limitação quando se trata de ciclos. No exemplo a seguir, dois objetos são criados e referenciam um ao outro, criando assim um ciclo. Eles sairão do escopo após a chamada de função, portanto, serão efetivamente inúteis e poderão ser liberados. No entanto, o algoritmo de contagem de referências considera que, como cada um dos dois objetos é referenciado pelo menos uma vez, nenhum deles pode ser coletado como lixo.

function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1 references o2
o2.p = o1; // o2 references o1. This creates a cycle.
}

f();

Algoritmo Mark-and-Sweep

Para decidir se um objeto é necessário, esse algoritmo determina se o objeto é alcançável.

O algoritmo de Mark-and-sweep passa por estas três etapas:

  1. Raízes: Em geral, as raízes são variáveis ​​globais que são referenciadas no código. No JavaScript, por exemplo, uma variável global que pode atuar como raiz é o objeto “window”. O objeto idêntico no Node.js é chamado de “global”. Uma lista completa de todas as raízes é construída pelo coletor de lixo.
  2. O algoritmo então inspeciona todas as raízes e seus filhos e os marca como ativos (o que significa que eles não são lixo). Qualquer coisa que uma raiz não alcance será marcada como lixo.
  3. Finalmente, o coletor de lixo libera todas as partes da memória que não estão marcadas como ativas e retorna essa memória ao sistema operacional.
Uma visualização do algoritmo de marca e varredura em ação

Este algoritmo é melhor que o anterior, já que “um objeto tem referência zero” faz com que este objeto seja inacessível. O oposto não é verdade como vimos com ciclos.

A partir de 2012, todos os navegadores modernos enviam um coletor de lixo de Mark-and-sweep. Todas as melhorias feitas no campo da coleta de lixo do JavaScript (coleta de lixo geracional/incremental/concorrente/paralela) nos últimos anos são melhorias de implementação desse algoritmo (mark-and-sweep), mas não melhorias sobre o próprio algoritmo de coleta de lixo, nem seu objetivo de decidir se um objeto é acessível ou não.

Neste artigo , você pode ler com mais detalhes sobre o rastreamento de coleta de lixo que também abrange a marcação e varredura junto com suas otimizações.

Os ciclos não são mais um problema

No primeiro exemplo acima, depois que a chamada de função retorna, os dois objetos não são mais referenciados por algo alcançável do objeto global.Consequentemente, eles serão encontrados inacessíveis pelo coletor de lixo.

Mesmo que haja referências entre os objetos, eles não podem ser acessados ​​pela raiz.

Comportamento contra-intuitivo dos coletores de lixo

Embora os Coletores de Lixo sejam convenientes, eles vêm com seu próprio conjunto de compensações. Um deles é o não determinismo . Em outras palavras, os GCs são imprevisíveis. Você não pode dizer quando uma coleção será executada. Isso significa que, em alguns casos, os programas usam mais memória, o que é realmente necessário. Em outros casos, as pausas curtas podem ser notadas em aplicações particularmente sensíveis. Embora o não-determinismo signifique que não se pode ter certeza quando uma coleta será realizada, a maioria das implementações do GC compartilha o padrão comum de fazer passes de coleta durante a alocação. Se nenhuma alocação for executada, a maioria dos GCs permanece inativa. Considere o seguinte cenário:

  1. Um conjunto considerável de alocações é executado.
  2. A maioria desses elementos (ou todos eles) são marcados como inacessíveis (suponha que anulemos uma referência apontando para um cache que não precisamos mais).
  3. Nenhuma alocação adicional é executada.

Nesse cenário, a maioria dos GCs não executará mais nenhum passe de coleta.Em outras palavras, embora existam referências inacessíveis disponíveis para coleta, elas não são reivindicadas pelo coletor. Estes não são estritamente vazamentos, mas ainda assim, resultam em uso de memória maior do que o habitual.

Quais são os vazamentos de memória?

Assim como a memória sugere, vazamentos de memória são pedaços de memória que o aplicativo usou no passado, mas não é mais necessário, mas ainda não retornou ao sistema operacional ou ao pool de memória livre.

Linguagens de programação favorecem diferentes maneiras de gerenciar a memória. No entanto, se um determinado pedaço de memória é usado ou não é realmente um problema indecidível . Em outras palavras, apenas os desenvolvedores podem deixar claro se uma parte da memória pode ser retornada ao sistema operacional ou não.

Certas linguagens de programação fornecem recursos que ajudam os desenvolvedores a fazer isso. Outros esperam que os desenvolvedores sejam completamente explícitos sobre quando uma parte da memória não é utilizada. A Wikipedia tem bons artigos sobre gerenciamento manual e automático de memória.

Os quatro tipos comuns de vazamentos JavaScript

1: variáveis ​​globais

O JavaScript lida com variáveis ​​não declaradas de uma maneira interessante: quando uma variável não declarada é referenciada, uma nova variável é criada no objeto global . Em um navegador, o objeto global seria window , o que significa que

function foo(arg) {
bar = "some text";
}

é o equivalente de:

function foo(arg) {
window.bar = "some text";
}

Digamos que o propósito da bar seja apenas referenciar uma variável na função foo. Uma variável global redundante será criada, no entanto, se você não usar var para declará-lo. No caso acima, isso não causará muito dano.Você pode certamente imaginar um cenário mais prejudicial.

Você também pode criar acidentalmente uma variável global usando this :

function foo() {
this.var1 = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();

Você pode evitar tudo isso adicionando 'use strict'; no início do seu arquivo JavaScript, que acionaria um modo muito mais estrito de analisar JavaScript, o que impede a criação inesperada de variáveis ​​globais.

Globais inesperados certamente são um problema, no entanto, na maioria das vezes, seu código estaria infestado de variáveis ​​globais explícitas que, por definição, não podem ser coletadas pelo coletor de lixo. Atenção especial deve ser dada às variáveis ​​globais usadas para armazenar e processar temporariamente grandes informações. Use variáveis ​​globais para armazenar dados se você precisar, mas quando fizer isso, certifique-se de atribuí-lo como nulo ou reatribuí-lo quando terminar.

2: Timers ou callbacks esquecidos

Vamos pegar setInterval por exemplo, como é frequentemente usado em JavaScript.

As bibliotecas que fornecem observadores e outros instrumentos que aceitam retornos de chamada geralmente garantem que todas as referências aos retornos de chamada se tornem inacessíveis, uma vez que suas instâncias também não podem ser acessadas. Ainda assim, o código abaixo não é um achado raro:

var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //This will be executed every ~5 seconds.

O snippet acima mostra as conseqüências do uso de timers que referenciam nós ou dados que não são mais necessários.

O objeto renderer pode ser substituído ou removido em algum ponto, o que tornaria o bloco encapsulado pelo manipulador de intervalo redundante. Se isso acontecer, nem o manipulador nem suas dependências serão coletadas, pois o intervalo precisará ser interrompido primeiro (lembre-se de que ele ainda está ativo). Tudo se resume ao fato de que serverData que certamente armazena e processa cargas de dados, também não será coletado.

Ao usar observadores, você precisa certificar-se de fazer uma chamada explícita para removê-los quando terminar com eles (ou o observador não será mais necessário ou o objeto ficará inacessível).

Felizmente, a maioria dos navegadores modernos faria o trabalho para você: eles coletariam automaticamente os manipuladores de observadores assim que o objeto observado se tornasse inacessível, mesmo se você tivesse esquecido de remover o ouvinte. No passado, alguns navegadores não conseguiam lidar com esses casos (o bom e velho IE6).

Ainda assim, está de acordo com as melhores práticas para remover os observadores quando o objeto se tornar obsoleto. Veja o seguinte exemplo:

var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
counter++;
element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);// Do stuffelement.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers // that don't handle cycles well.

Você não precisa mais chamar removeEventListener antes de tornar um nó inacessível, pois os navegadores modernos suportam coletores de lixo que podem detectar esses ciclos e manipulá-los adequadamente.

Se você aproveitar as APIs do jQuery (outras bibliotecas e frameworks suportam isso também), você também pode remover os listeners antes que um nó se torne obsoleto. A biblioteca também se certifica de que não haja vazamentos de memória mesmo quando o aplicativo estiver sendo executado em versões mais antigas do navegador.

3: Closures

Um aspecto chave do desenvolvimento do JavaScript são closures: uma função interna que tem acesso às variáveis ​​da função externa (envolvente).Devido aos detalhes de implementação do tempo de execução do JavaScript, é possível vazar memória da seguinte maneira:

var theThing = null;var replaceThing = function () {var originalThing = theThing;
var unused = function () {
if (originalThing) // a reference to 'originalThing'
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);

Uma vez que replaceThing é chamado, theThing obtém um novo objeto que consiste em um array grande e um novo closure ( someMethod ). No entanto, originalThing é referenciado por um closure que é mantido pela variável unused (que é a variável theThing da chamada anterior para replaceThing ). O importante é lembrar que, quando um escopo para closures for criado para closures no mesmo escopo pai, o escopo será compartilhado.

Neste caso, o escopo criado para o fechamento someMethod é compartilhado com unused. unused tem uma referência ao originalThing . Mesmo que o unused não utilizado nunca seja usado, someMethod pode ser usado através do theThing fora do escopo de replaceThing (por exemplo, em algum lugar globalmente). E como someMethod compartilha o escopo de encerramento com unused , a referência unused tem que originalThing força-o a permanecer ativo (todo o escopo compartilhado entre os dois fechamentos).Isso evita sua coleção.

No exemplo acima, o escopo criado para o fechamento someMethod é compartilhado com unuse, enquanto unused referências unusedoriginalThing. someMethod pode ser usado através do theThing fora do escopo replaceThing , apesar do fato de que unused usado nunca é usado. O fato de que as referências não utilizadas originalThing exigem que ele permaneça ativo, pois someMethod compartilha o escopo de fechamento com não utilizado.

Tudo isso pode resultar em um vazamento de memória considerável. Você pode esperar um aumento no uso de memória quando o snippet acima for executado repetidas vezes. Seu tamanho não diminui quando o coletor de lixo é executado. Uma lista vinculada de closures é criada (sua raiz é a variável theThing nesse caso) e cada um dos escopos de fechamento leva adiante uma referência indireta ao array grande.

Este problema foi encontrado pela equipe Meteor e eles têm um ótimo artigo que descreve o problema em grande detalhe.

4: Fora das referências do DOM

Há casos em que os desenvolvedores armazenam nós DOM dentro de estruturas de dados. Suponha que você queira atualizar rapidamente o conteúdo de várias linhas em uma tabela. Se você armazenar uma referência a cada linha DOM em um dicionário ou em uma matriz, haverá duas referências ao mesmo elemento DOM: uma na árvore DOM e outra no dicionário. Se você decidir se livrar dessas linhas, lembre-se de tornar as duas referências inacessíveis.

var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// The image is a direct child of the body element.
document.body.removeChild(document.getElementById('image'));
// At this point, we still have a reference to #button in the
//global elements object. In other words, the button element is
//still in memory and cannot be collected by the GC.
}

Há uma consideração adicional que deve ser levada em conta quando se trata de referências a nós internos ou folha dentro de uma árvore DOM. Se você mantiver uma referência a uma célula de tabela (uma tag <td> ) em seu código e decidir remover a tabela do DOM, mantendo a referência a essa célula específica, poderá esperar que um grande vazamento de memória ocorra. Você pode pensar que o coletor de lixo liberaria tudo, menos essa célula. Este não será o caso, no entanto. Como a célula é um nó filho da tabela e os filhos mantêm referências a seus pais, essa única referência à célula da tabela manteria a tabela inteira na memória .

Nós, no SessionStack, tentamos seguir essas práticas recomendadas ao escrever código que lida corretamente com a alocação de memória, e aqui está o porquê:

Uma vez que você integra o SessionStack no seu aplicativo web de produção, ele começa a registrar tudo: todas as alterações do DOM, interações do usuário, exceções do JavaScript, rastreamentos de pilha, solicitações de rede com falha, mensagens de depuração, etc.
Com o SessionStack, você reproduz problemas em seus aplicativos da Web como vídeos e vê tudo o que aconteceu com seu usuário. E tudo isso deve ocorrer sem impacto no desempenho do seu aplicativo da web.
Como o usuário pode recarregar a página ou navegar em seu aplicativo, todos os observadores, interceptores, alocações de variáveis, etc. precisam ser manipulados adequadamente, para que não causem vazamentos de memória ou não aumentem o consumo de memória do aplicativo da Web que estamos integrados.

Existe um plano gratuito para que você possa experimentá-lo agora .

Referências

Este é um artigo traduzido com a autorização do autor. O artigo original pode ser lido em https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec

Autor do post original — Alexander Zlatkov — Co-founder & CEO @SessionStack

--

--

Robisson Oliveira
React Brasil

Senior Cloud Application at Amazon Web Services(AWS)