RSS Feed

Memory Leak

March 17, 2009 by admin

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 memory leaks

 



1 Comment »

  1. 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é!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>