Contents |
Identificação do problema
Quando desenvolvemos aplicativos com DHTML (Javascript, DOM e CSS) encontramos problemas para controlar a performance e os memory leaks. Uma vez identificados os padrões de códigos que causam esses problemas, começaremos a trabalhar de forma homogênea e sem a necessidade de retrabalho.
O problema não atinge só o Internet Explorer 6.0 (IE6). Ele pode acontecer com o Mozilla, Netscape e Opera. Apesar do IE6 ser o browser mais problemático hoje em dia. Muitos aplicativos, com determinado tempo de uso, começam a consumir uma quantidade impressionante de memória causando lentidão na máquina do usuário e refletindo na experiência que ele tem ao acessar o site. Muitas vezes a memória só é devolvida ao sistema operacional depois de fechar o browser.
Então o que acontece para existir o memory leak? Como é muito comum supor, o problema não está no garbage colector (GC) dos browsers. Pelo contrário. Eles funcionam muito bem se você tomar determinados cuidados na hora de programar. Considere o seguinte código:
function GCTest() {
var x = new Array(1000).join(new Array(2000).join('XXXXX'));
}
Após esta função ser executada, toda a memória utilizada pela variável x é devolvida para o sistema operacional pelo GC do Javascript (COM). O mesmo acontece quando trabalhamos com elementos do DOM. Quando criamos uma tabela com HTML, não precisamos nos preocupar em remover essa tabela da memória após a execução da página.
<table> <tr><td>Lorem</td><td>Ipsum</td></tr> </table>
Quando trabalhamos com os “dois mundos”, o COM guarda uma referência para cada elemento. Se existe referência para um elemento no COM, a memória usada pela por ele não é liberada. O problema acontece quando temos uma referência cruzada, onde um objeto Javascript aponta para um objeto DOM e o DOM aponta de volta para o Javascript, e assim a memória usada nunca será liberada.
Padrões de memory leak
Inline script
Na declaração inline de um script na criação de um elemento HTML a referência que é criada para a função foo() não é eliminada depois que o código é executado. Isso cria uma referência circular e causa o memory leak.
function LeakMemory() {
for (var i = 0; i < 5000; i++) {
var parentDiv = document.createElement(‘
} }
Nesse código criamos um novo elemento div sem a declaração do evento. O evento deve ser adicionado depois que o elemento já existe no DOM para evitar o memory leak. Note que nesse exemplo o número de iterações passa de 5.000 para 50.000, e mesmo assim a memória é liberada após a execução do escopo da função.
function LeakMemory() {
for (var i = 0; i < 50000; i++) {
var parentDiv = document.createElement('div');
}
}
Closures
Closures é um pattern muito utilizado em Javascript que permite o encapsulamento de funções. Leia mais sobre o assunto em Referências. Neste caso, adicionamos o evento onclick após a criação do elemento HTML.
function LeakMemory() {
var parentDiv = document.createElement('div');
parentDiv.onclick = function() {
foo();
};
}
Quando criamos o elemento parentDiv e adicionamos o evento onclick dentro da função principal, criamos um closure. Ele guarda uma referência no elemento DOM (parentDiv) e o Javascript (função anônima).
Expando property
No código a seguir, criamos uma referência circular guardando a referência da variável global do Javascript que aponta para um elemento DOM dentro de uma propriedade do elemento DOM. O objeto Javascript myGlobalObject aponta para o objeto DOM (div) LeakDiv que tem a propriedade expandoProperty que aponta de volta para o elemento Javascript myGlobalObject.
var myGlobalObject;
function SetupLeak() {
myGlobalObject = document.getElementById('LeakedDiv');
document.getElementById('LeakedDiv').expandoProperty = myGlobalObject;
}
Encapsulator pattern
Esse caso é muito semelhante ao anterior só que a referência circular é feita guardando a referência do DOM dentro da função Javascript e a referência Javascript dentro da propriedade do objeto DOM. O objeto Javascript this.elementReference aponta para o objeto DOM (div) element que tem a propriedade expandoProperty que aponta de volta para o elemento Javascript.
function Encapsulator(element) {
this.elementReference = element;
element.expandoProperty = this;
}
function SetupLeak() {
new Encapsulator(document.getElementById('LeakedDiv'));
}
Event listener
Esse é o caso mais comum de memory leak com closures, onde o evento onclick faz a referência circular por usar uma função anônima dentro do mesmo escopo da criação do listener.
window.onload = function() {
var obj = document.getElementById('LeakedDiv');
obj.onclick = function(e) {
... lógica ...
};
}
Cross Page Leak
Aqui temos um caso que é muito difícil de identificar. Nesse caso, a ordem de inserção dos novos elementos na árvore do DOM influencia. Na criação do parentDiv e do childDiv foi usado inline script e quando adicionamos os elementos na página ele perde a referência porque os elementos apontam para funções anônimas.
function LeakMemory() {
var hostElement = document.getElementById('hostElement');
for (i = 0; i < 5000; i++) {
var parentDiv = document.createElement(‘
var childDiv = document.createElement(‘
parentDiv.appendChild(childDiv); hostElement.appendChild(parentDiv); hostElement.removeChild(parentDiv); parentDiv.removeChild(childDiv); parentDiv = null; childDiv = null; } hostElement = null; }
Novamente, a melhor opção é trabalharmos com funções fora do escopo da criação dos elementos e, se necessário, ao inserir no DOM sempre fazermos a inserção do elemento pai e depois o elemento filho como é mostrado no exemplo seguinte.
function LeakMemory() {
var hostElement = document.getElementById('hostElement');
for (i = 0; i < 5000; i++) {
var parentDiv = document.createElement(‘
var childDiv = document.createElement(‘
hostElement.appendChild(parentDiv); parentDiv.appendChild(childDiv); parentDiv.removeChild(childDiv); hostElement.removeChild(parentDiv); parentDiv = null; childDiv = null; } hostElement = null; }
Boas práticas
Referências ao DOM
É recomendável que ao criar uma variável Javascript que aponte para um elemento DOM ela seja desalocada logo que deixar de ser usada. Para isso atribuímos null à variável. Essa regra se aplica tanto para variáveis privadas como para as privilegiadas.
function domRefecente() {
var header = document.getElementById('headerDiv');
var footer = document.getElementById('footerDiv');
this.div = document.createElement('div');
... lógica ...
header = null, footer = null, this.div = null;
}
Quando é necessário guardar um objeto DOM no Javascript para utilização futura, SEMPRE guarde o id do objeto ao invés de uma referência para ele.
function domRefecente() {
var div = document.createElement('div');
div.id = 'meuDiv';
var divId = div.id;
div = null;
}
Eventos
Não usar funções anônimas para os eventos. Exemplo da correta declaração de um evento.
function addListener() {
var elm = document.getElementById('myDiv');
elm.onclick = handleClick;
elm = null;
}
function handleClick(e) {
... lógica ...
}
Ao atribuir um novo evento a um elemento, ele já deve existir na árvore do DOM. Exemplo da correta atribuição de evento a um novo elemento que está sendo criado via Javascript.
function newElement() {
var elm = document.createElement('div');
document.appendChild(elm);
elm.onclick = handleClick;
elm = null;
}
function handleClick() {
... lógica ...
}
Quando um elemento é removido do DOM via Javascript, todos os eventos cadastrados nele devem ser removidos para evitar o memory leak.
function createElements() {
var ul = document.getElementById('menu');
for (var i = 0; i < 5000; i++) {
var li = document.createElement('li');
li.id = 'menu' + i;
ul.appendChild(li);
li = null;
}
addListener();
ul = null;
}
function addListener() {
for (var i = 0; i < 5000; i++) {
var li = document.getElementById('menu' + i);
li.onclick = handleClick;
li = null;
}
}
function handleClick(e) {
... lógica ...
}
function removeElements() {
for (var i = 0; i < 5000; i++) {
var li = document.getElementById('menu' + i);
li.onclick = null;
li.parentChild.removeChild(li);
li = null;
}
}
Criando elementos no DOM
A ordem com que os elementos são inseridos no DOM via Javascript influencia na performance e na criação de memory leaks. O ideal é termos uma outra função fora do escopo da função usada para a criação dos elementos onde serão atribuídos os eventos a esses elementos.
Quando estamos inserindo uma série de elementos no DOM, a cada appendChild o browser renderiza esse elemento juntamento com todos os filhos do elemento (elemento pai) aonde estamos inserindo ele. O que isso significa? Quando temos um div com vários div’s dentro dele, ao inserir um novo div, todos os anteriores serão renderizados novamente, consumindo uma quantidade enorme de tempo proporcional a quantidade de elementos no div pai.
Nesse caso usamos a técnica de criar um elemento temporário, adicionar todos os div’s filhos dentro dele e só então adicioná-los ao div pai.
function addDiv() {
var divPai = document.getElementById('divPai');
var divTmp;
for (var i = 0; i < 5000; i++) {
var divFilho = document.createElement('div');
divTmp.appendChild(divFilho);
divFilho = null;
}
divPai.appendChild(divTmp);
divPai = null, divTmp = null;
}
Existe o caso em que precisamos adicionar o evento nos elementos na hora da sua criação. Para tornar isso possível, podemos inverter a ordem da inclusão dos elementos como mostrado no item Cross Page Leak.
Conclusão
Com um pouco de atenção e conhecimento podemos evitar muita dor de cabeça no futuro. Facilitando a manutenção do código e o acesso dos usuários aos nossos sites. O memory leak é apenas um dos problemas que temos de nos preocupar na hora de criar um aplicativo web. Por isso que um pouco de planejamento antes de começar a codificar evita muitas horas de depuração de código e retrabalho.
Programas
Veja no item específico da lista de Softwares para Interface #Memory Leak.
Referências
- Javascript Closures
- Memory Leakage in Internet Explorer – revisited
- Memory leak patterns in JavaScript
- IE: where’s my memory?
- DHTML Leaks Like a Sieve
- Javascript memory leaks

Leandro, encontrei seu texto procurando por Closures. Como não sou programador, ainda estou analisando o que li, mas encontrei algo que merece comentário no seu texto.
Ao invés de criar um elemento temporário, por quê não criar um documentFragment ou, dependendo do caso, clonar o elemento que receberia childNodes e substituí-lo via parentNode.replaceChild? No meio de um texto que prega pelas boas práticas, esse detalhe me pareceu um intruso.
Estou a disposição para comentar o assunto.
Até!