Aha! Memory leak fixed; a summary
Ok. So in my previous posts I talked a lot about various ways to detect javascript leaks, what often causes them, etc. I have to libraries that are leaking, both of which use Objects that are instantiated and reference, in one form or another, elements in the DOM.
I’ve fixed my memory leaks in my popup handler (which I’ll post about in a sec) but not yet in my toolbar script (which manages to leak in Firefox if you can believe it). My popup handler leaked for two reasons: circular references and closures.
Here are the 3 rules I have to offer to avoid these leaks:
1) NEVER store elements in the ‘this’ of an object.’
Variables that reference a DOM element - example:
var myDiv = document.getElementById('myDiv');
store that element in memory. In IE, the DOM has its own garbage collection that is seperate from the javascript collector. If the DOM garbage collector tries to clean up its memory when the page unloads and finds an element that is referenced by the Javascript, it leaves it. The javascript garbage collector… does the same thing for DOM elements referenced in javascript. Thanks Microsoft!
Normally this isn’t a problem because these references are one way. I.e. the DOM element references some javascript (myDiv.onclick = someFunction…) or vice versa. But when you have your javascript referencing the DOM element also, you create a circular reference. Now neither garbage collector can clean up the DOM element or the function/variable that references it because they each reference each other.
Variables that reference a DOM element and stored in the ‘this’ of an object
like so:
var myClass = Class.create();
myClass.prototype = {
initialize: function(elementId) {
this.element = $(elementId);
}
}
Create a reference to that element in memory. This makes it exceptionally easy to end up with a circular reference in numerous ways (event handling being the most prominent). So, don’t do it. Store the id of the element and reference it in your object as $(id) over and over again. The fact that the Prototype $() function can take both an element and an id makes this pretty easy to implement.
2) NEVER store elements in variables defined outside a function (i.e. in the window) and
3) NEVER construct a function or an Object when an element is stored in a variable in scope. When a function is constructed, it has access to every element in the current scope. This means that if you define a variable that references a DOM element and then construct a function (in the same scope) that function now contains those same references. This is called closure (or lack there off) and the result is pretty much the same as circular references. Neither your object/function nor the DOM elements will be collected.
Example:
var myDiv = document.getElementById('myDiv');
function sayHi() {
alert('hi');
}
the function sayHi(); now has a reference to $(’myDiv’); They don’t look related, but they are. This is why you shouldn’t create these references in the global (i.e. the window) scope.
Here’s another pitfall:
function sayHi(observeId, message) {
var el = $(observeId);
Event.observe(el, "click", function() {
alert(message);
});
}
The problem is that you have an element on the stack (el) and you define a function (Event.observe(….function(){…});
That function is now tied to your DOM element and neither will be collected.
So, to sum up:
NEVER store elements in the ‘this’ of an object.’
NEVER store elements in variables defined outside a function (i.e. in the window) and
NEVER construct a function or an Object when an element is stored in a variable in scope.
Hi !
Really realy interesting article !
Well I have a question about your 1st example about myClass.prototype{ … }
In my code I reference: “this.element = $(element);” because my element doesn’t always have an id. BUT in an onunload I clean all my references. Is it a right solution ?
With Mootools, this is no longer a problem. Storing a circular reference still is trouble though.
initialize: function(el){
this.el = $(el); //this is ok
this.el.fx = new Fx.Style('height'); //circ. reference - bad!
}
});
Hi!
Thanks for writing this stuff. I am hunting memory leaks in my JS code right now and your article is really useful.
Would you be so kind to provide a Javascript example illustrating the third point?
Thank you,
Temuri
That third point is all about closures. There is a simple example above about this topic, but if you're using Mootools, this issue is largely resolved, too. The nutshell is that if you have a document element stored in a variable namespace and you create a function in that scope, that pointer will remain around because the function keeps a reference to everything in its scope.
Hi... Thanks for this post.
About the last example. Would this 'fix' that closure?
function sayHi(observeId, message) {
var el = $(observeId);
Event.observe(el, "click", function() {
alert(message);
});
el = null;
}
OR
function sayHi(observeId, message) {
Event.observe($(observeId), "click", function() {
alert(message);
});
}
Could you please send me an email with your response? Thank you..
I can't really say for Prototype, as I haven't used it in a long time. As I note in a comment above, Mootools doesn't have these leak issues anymore due to its own garbage collection methods. It's possible (even probable) that Prototype has this kind of functionality, too, but I don't know.
As for your code example, no, the problem (when I authored this, and again, it was about Prototype), was to do with closures. The basic concept was that you have a reference from javascript to an element (this.el = $(el)) and then a reference from the element back to the javascript (by attaching an event with the function defined). That function has a reference to any variable in scope, so even if you say that the element is now nil, it won't help. You need to explicitly remove the reference from the element to the function attached to it, which is exactly what the Mootools Garbage collector does.
Again, I suspect that most frameworks are doing this sort of thing for you now...