Javascript memory leaks

Quite by accident I found the article DHTML Leaks Like a Sieve by Joel Webber. It's an interesting read that I can recommend to all JavaScripters. Also, it may have disturbing implications for my current coding practices.

This whole memory thing seems to be more important and more complicated than I thought. I have absolutely zero knowledge of software development and related skills, so I never worried about memory leaks, even though, having carefully read through the relevant paragraphs in Flanagan's Def Guide, I sort of vaguely understand what it's all about.

Basically, Webber says that the browsers inevitably take up more and more system memory even with relatively simple JavaScripts, which may become a problem in the long run. That's something that could impact me directly, because one of my current favourite tricks seems to be vulnerable to memory leaking.

Take a simple structure like this:

<h3>Header</h3>
<div>[more info]</div>

Clicking on the H3 opens/closes the DIV and changes the style of the H3. This script works roughly as follows:

window.onload = function () {
	var x = document.getElementsByTagName('H3');
	for (var i=0;i<x.length;i++)
	{
		x[i].onclick = openClose;
		x[i].relatedElement = x[i].nextSibling; // simplified situation
		x[i].relatedElement.relatedElement = x[i];
	}
}

var currentlyOpened;

function openClose()
{
	if (currentlyOpened)
	{
		currentlyOpened.style.display = 'none';
		currentlyOpened.relatedElement.className = '';
	}
	this.relatedElement.style.display = 'block';
	this.className = 'highlight';
	currentlyOpened = this;
}

As you'll notice I set the H3 and DIV to refer to each other as their relatedElement. I find this a very useful trick in many circumstances, since finding out which other element should be influenced by a click on one element becomes very easy. (In this simple example I don't really need the second relatedElement, but in more complex scripts I do)

Nonetheless this seems to trigger memory leaks. The H3 and DIV (and there are several pairs!) refer to each other, which means the memory they take up can't be garbage collected — at least, I hope that's correct. As I said I know very little about these matters.

Even though I vaguely understood the danger before reading Webber's article, I thought it didn't matter much, because I link only a few elements, and when the user leaves the page the memory would be reclaimed anyway, or so I thought.

However, according to Webber this last part is untrue. Even when the user loads a new page and destroys the old one, the memory doesn't seem to be freed. And that's what's bugging me. Why is this so? Do I have to significantly rethink my approach of having two JavaScript objects refer to each other? Can two JavaScript objects refer to each other at all without triggering unpleasantness on the user's computer after a while? I'd like to continue using this trick.

Questions, questions...

This is the blog of Peter-Paul Koch, web developer, consultant, and trainer. You can also follow him on Twitter or Mastodon.
Atom RSS

If you like this blog, why not donate a little bit of money to help me pay my bills?

Categories:

Comments

Comments are closed.

1 Posted by Dejan Kozina on 11 February 2005 | Permalink

I've googled for "unhook event handlers", but found very few entries for Javascript and no explanation at all. Any hint on the right way to do it?

2 Posted by peter royal on 11 February 2005 | Permalink

i read that too (after reading the google maps analysis he did :)

it all made sense up until the same point.. why on earth would the browser *not* free memory when the window is closed? i can't think of a reason why it would be.. yes, you may have circular references, but once a circular reference is totally detached from the root of the entire object graph, it can be freed.

3 Posted by Jim Ley on 11 February 2005 | Permalink

The problem is that IE cannot breakdown closures over DOM objects. Richard Cornford wrote this up nicely in the notes to the comp.lang.javascript FAQ - see: http://jibbering.com/faq/faq_notes/closures.html#clMem

4 Posted by Mark Wubben on 11 February 2005 | Permalink

ppk, it's references between JS and COM objects which cause problems, not between JS objects.

I have to say I had no idea this could happen in Mozilla too, perhaps I'll do a test just to see it for myself.

5 Posted by Marcelo Volmaro on 11 February 2005 | Permalink

I think the correct way to do it is (untested, but it should work):

someobject.eventListener = undefined;
or
delete someobject.eventListener;

6 Posted by Jason Brunette on 11 February 2005 | Permalink

Nice article. Especially the "don't forget to unhook all of your event handlers" statement. This helped me remove a 1MB-leak-per-refresh problem I was having with my JS event manager object and IE.

7 Posted by sam on 11 February 2005 | Permalink

I'm not confident I understand the problem fully, but could it be solved by keeping track of IDs, rather than of the DOM objects themselves? You'd have to do lots more getElementById calls, but you wouldn't have any javascript that references DOM elements...

8 Posted by ppk on 11 February 2005 | Permalink

Mark, please explain further. When would I reference a COM object?

9 Posted by Eric O'Connell on 11 February 2005 | Permalink

ppk, from Webber's article, the COM objects seem to be how the browsers internally represent DOM elements. Thus, a DIV is an COM object. So, when you create a circular reference between the javascript object and the DIV, the browser never knows when it can delete either. Thus, it never does.

10 Posted by John Serris on 11 February 2005 | Permalink

I'm no expert either but I remember reading about this on Mihai's site first:
http://www.bazon.net/mishoo/articles.epl?art_id=824

11 Posted by ppk on 16 February 2005 | Permalink

Eric, Mark, I still don't understand. In the code example in the entry, do I refer to a COM object or not?

12 Posted by Ian on 16 February 2005 | Permalink

I have run into this problem at work. We're building a Javascript library for building applications that run in the browser. Sort of like a cross-browser XUL, but that's a bit of an overstatement.

Anyway, here's the problem: in Internet Explorer, the DOM is one COM component, and the JScript engine is a separate COM component. To be honest with you, I'm a little fuzzy on what exactly "COM component" means, but I think it's enough to know that they are separate chunks of compiled code that don't necessarily know about each other. Well, the DOM component is "garbage collected", as is the JScript component, which means that if you create an object within either component, and then lose track of that object, it will eventually be cleaned up. For example:

function makeABigObject() {
var bigArray = new Array(20000);
}

When you call that function, the JScript component creates an object (named bigArray) that is accessible within the function. As soon as the function returns, though, you "lose track" of bigArray because there's no way to refer to it anymore. Well, the JScript component realizes that you've lost track of it, and so bigArray is cleaned up--it's memory is reclaimed. The same sort of thing works in the DOM component. If you say "document.createElement('div')", or something similar, then the DOM component creates an object for you. Once you lose track of that object somehow, the DOM component will clean up the related memory.

Now, IE leaks memory when you have references that bridge the gap between the DOM component and the JScript component because neither component knows anything about the other one, so neither can tell if a reference is really gone. For example:

function Foo(div) {
this.div = div;
div.foo = this;
}

function leakMemory() {
var div = document.createElement('div');
var foo = new Foo(div);
}

leakMemory()

This example code looks like the first example. You would think that when leakMemory() returns, both objects would be cleaned up because you can't refer to either anymore. The problem is that, while the DOM component knows that no other DOM object refers to div, and that foo is the only JScript object that refers to div, it doesn't know that no JScript object refers to foo, so it doesn't clean up div. The reverse is also true: the JScript component knows that no JScript object refers to foo, and it knows that div is the only DOM object that refers to foo, but it doesn't know that no DOM object refers to div, so it doesn't clean up foo, either. This is a memory leak: you can't use either of foo or div, so the memory that they consume is "garbage" and should be cleaned up, but the browser doesn't "know" that, so it hangs on to both objects until the browser shuts down.

The solution is to clean up yourself. It can be a real pain, but it's the only way to keep the user's browser from bloating up. This code, similar to the code from above, doesn't (or at least shouldn't) leak:

function Foo(div) {
this.div = div;
div.foo = this;
}

function cleanUpAFoo(foo) {
foo.div.foo = null;
foo.div = null;
}

function dontLeakMemory() {
var div = document.createElement('div');
var foo = new Foo(div);

cleanUpAFoo(foo);
}

dontLeakMemory();

The other solution is to never use expando properties on DOM objects, but obviously this can be very limiting.

Ian

13 Posted by Vincenzo on 18 February 2005 | Permalink

As Ian pointed out, the problem arises when a javascript object and a DOM object refers to each other.

One solution is to use only one object reference, for example:

var div = document.createElement("div");
div.id = getUniqueId();
var obj = new Object();
div.myobj = obj;
obj.mydivid = div.id;

function getUniqueId() {
if(!document.uid) { document.uid = 0; }
return "myuid_" + (++document.uid);
}

So if you need to access the obj from the div you just use div.obj, while if you need to access the div from the obj you have to use:
document.getElementById(obj.mydivid);

Then you can forget about object cleanups

14 Posted by Tom Trenka on 4 March 2005 | Permalink

Some attempts at explaining above, not very clear though, so allow me :)

A COM object, in MS parlance, is an object that implements a single interface (IUnknown, I know, I'm getting very tech here, I promise it gets a little easier). The idea behind this structure is that someone can create a COM class, and anything within Windows that can handle COM objects can handle the custom class.

Now MS took this concept and ran with it with the overwhelming majority of Windows code. Including within IE. It's not that the DOM is a COM object; it's that every single node within a document is a COM object, and designed to operate independantly of it's host--including having it's own garbage collection.

Now with MS, the JScript engine happens to also be an independant COM object. You may not realize this, but that engine is used for a lot more in Windows than just IE...it's used with Windows Shell Scripts, it's used as one of two default scripting engines for ASP, etc. This, of course, means that it too has it's own independant garbage collection mechanism.

Hopefully this sheds some light on why the leaks happen. As for your code specifically...

window.onload = function () {
var x = document.getElementsByTagName('H3');
for (var i=0;i<x.length;i++)
{
x[i].onclick = openClose;
x[i].relatedElement = x[i].nextSibling; // simplified situation
x[i].relatedElement.relatedElement = x[i];
}
}

var currentlyOpened;
function openClose()
{
if (currentlyOpened)
{
currentlyOpened.style.display = 'none';
currentlyOpened.relatedElement.className = '';
}
this.relatedElement.style.display = 'block';
this.className = 'highlight';
currentlyOpened = this;
}

I've "highlighted" the main issues. To begin with, when you set the onclick event to a function reference, you've created the first leak; while this would seem to be good practice (since you've only created one function), you (probably) never break this reference onunload. True, you'd expect the browser to do it, but...sigh.

Setting expando properties on nodes (which in IE are COM objects) forces the JScript engine to create a wrapper around the COM object in question; this is like creating a closure without actually defining it. Bad idea. Sigh.

Setting currentlyOpened to "this" isn't such a bad thing, but like with the onclick method, you probably never break the reference onunload.

There's a little more here, but those are the most salient points.

Now...the issue with Moz is that internally, they actually copied some of the better aspects of the COM object model (they call it XPCOM, for "Cross Platform COM"). The architecture has some advantages and disadvantages over the MS model (the big one being it is only a host, not an operating system), but with that architecture you also get some of the same leaks.

The moral? Use the window.onunload method if it's available to break these references, and maybe one of these days I or someone like me will go through and figure out a way of breaking references on things set with addEventListener... :)

15 Posted by Adam Kerz on 25 March 2005 | Permalink

Just wondering whether anyone has tried (onunload) a simple:

var alltags=document.getElementsByTagName("*");

for(var i=0;i<alltags.length;i++){
    alltags[i].onclick=undefined;
    // and so on for other event handlers
}


or does the reference need to be removed on the js side too?

16 Posted by vivek on 6 June 2005 | Permalink

Our developers have found that minimizing the browser window clears the memory acquired by the leak. Does any body know what happens on minimizing the window that can clear the memory. Also how can we call that mechanism from the Internet browser itself..

17 Posted by Gregory Wild-Smith on 10 June 2005 | Permalink

Well Vivek - your developers are wrong. Sort Of.

It does free it while minimised, to allow other applications to use it, but you'll notice that it creeps up again pretty quickly when you restore application.

The window minimise behavior is standard windows behavior and has nothing to do with JS or the browser.