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...
Category archives:
Monthlies:
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
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];
}
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.
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?