Memory Leaks in Microsoft Internet Explorer #
I originally posted this at isaacschlueter.com on Monday, October 23rd, 2006.
Memory Leaks.
What are they? How do they happen? What can be done about them?
This is a great question, and a topic that has a lot of mysticism surrounding it. Like most Javascript issues, thereâs been a lot of very bad âauthoritativeâ suggestions.
If you are a webdev interviewing at Yahoo!, and Iâm in the room, I can guarantee that you will be asked about this topic. In my opinion, memory leaks are one of the most tricky ways in which a userâs web experience can be needlessly degraded. A web developerâs attitude towards memory leaks is, in my opinion, one of the best indicators of their worth in this field.
Either you understand what a memory leak is and how it happens, or you donât. If you donât, thatâs fine, but itâs time to find out once you hear some buzz about it. Once you understand it, you can either care about it, or not. Anyone who doesnât fall into the âfind out/care about itâ category would be better off flipping burgers than writing Javascript.
What Are Memory Leaks?
In general, a âmemory leakâ is the failure to release memory that has been allocated to a program. Over time, a memory leak will result in progressively less memory being available to perform valid functions.
In the browser/Javascript world, a memory leak occurs when the native garbage collection routines donât properly reclaim the memory that was allocated for an object. Each time you create an object in Javascript, a bit of memory is allocated. Memory is also allocated for DOM nodes, COM objects, images, etc. When an object can no longer be accessed, it is flagged as âready for removal.â Then the garbage collection routine sweeps up the flagged objects, and releases the memory back to the system. In Microsoft Internet Explorer 6, there is a bug in the garbage collection routine that can lead to memory leaks in certain conditions.
A good discussion on memory leaks and why and how they happen can be found on Crockfordâs site. The claim that closures (functions inside of other functions) cause memory leaks is, as Crockford says, âdeeply wrong.â Closures are fine, and are not the source of the problem. (However, of course, closures can make circular links trickier to spot.)
The problem happens when you have a Javascript object and DOM node (or any COM object) that refer to one another in a cycle. IE 6 canât figure out when it should reclaim the memory, so it doesnât ever do it.
For example, this will cause a leak:
(function(){
var obj={b:document.body};
document.body.o=obj; // â circular link is created. document.body.o.b === document.body
})();
If you set either obj.doc.body or body.o to NULL, then youâll break the circular chain, and IE 6 will reclaim the memory.
The cycle doesnât have to be so small. Even a chain of many steps can cause a leak if it is not broken. This will cause a leak, too:
(function(){
var d={b:document.body}
var obj={doc:d}; // â obj.doc.b === document.body
document.body.o=obj; // â Circular loop: document.body.o.doc.b === document.body
})();
Sometimes the references arenât explicit, but are created by a scope closure. (This may be part of the thinking behind the assertion that closures cause memory leaks.) For example, this will also leak:
(function(){
var b=document.body; // â create a reference to document.body inside of the outer scope.
b.onclick=function() { // â b.onclick refers to a function.
// this function can access "b" due to closure
// do something...
};
})();
The anonymous function is assigned to document.body.onclick
. Due to the scope closure, there is a reference (b
) created inside of the anonymous function that points back at document.body. This creates a circular condition that confounds and befuddles the MSIE garbage collector just like the other examples above.
How To Avoid Memory Leaks
The simplest way to ensure that you will never have a memory leak is to simply never have circular reference chains that cross between Javascript and DOM space. Make sure that you always have Javascript objects refer to DOM objects, and never the other way round, or vice versa.
However, itâs sometimes extremely convenient to have circular link. Consider this example:
(function(){
var doSomething=function(e) {
this.innerHTML='did something!';
this.object.doSomethingElse(this.customPropertyOfSomeKind);
};
myDomNode.object=new myObject();
myDomNode.customPropertyOfSomeKind={some:'data object'};
YAHOO.util.Event.addListener(myDomNode,'click',doSomething);
})();
Now, if myObject has any reference to myDomNode (even if it refers to something that refers to something else ⌠that refers to myDomNode), youâll leak memory.
How to Fix MSIEâs Javascript Memory Leaks
So, how to fix this?
First, be aware when youâre doing things that may cause a leak. If itâs not a very big gain in code simplicity, then figure out another way around. If youâre hanging a lot of Javascript objects onto DOM objects, thereâs a big chance of a leak creeping in. Personally, I try to make sure that all my references go from JSâDOM and not the other way around. If the references are always one-way, then thereâs no chance of a leak. (Closures can only create JSâDOM references.) Also, weâve seen performance issues with hanging too much stuff on DOM nodes if the page is big and complicated (thatâs anecdotal, and I donât have any hard numbers, so take it for what itâs worth.) If you understand how they work and why they happen, you can save yourself a lot of time later on tracking them down.
Second, test your code with Drip. Test early, test often, and always test before you release to production.
I canât possibly stress how important this is. Even if youâve done everything right, itâs easy to overlook circular references if the code gets sufficiently complex. Even small memory leaks can add up over the course of a session, or in a browser that stays open for days on end. (It happens quite a bit. I donât know when I last closed the browser on my home computer, and a lot of users are the same way.)
Third, if you must cause circular references in your code, be responsible about it. Save a reference to each afflicted DOM node, and break the cycles on window unload. I often do something like this:
(function(){
var unLoaders=[];
myDomNode.object=new myObject(); // â let's say that this creates a leak somewhere
unLoaders.push(myDomNode); // â save it for later
// create an "unload" function
var unload=function(){
for(var i=unLoaders.length-1;i>-1;i--){
unLoaders[i].object=null; // â break the cycle
}
};
// run the unload function on window.unload
YAHOO.util.Event.addListener(window,'unload',unload);
})();
Memory Leaks and Ajax/XHR
Ajax is a huge source of memory leaks, and unfortunately in this day and age of fast-prototyping and marketing pushing, a lot of these errors slip out into the world. If you donât use a polling mechanism of some kind, AJAX applications will leak memory like a bucket with no bottom. Use the YUI connection library for Ajax, and never look back. Itâs brilliant, and very easy to use.
So, why do AJAX apps leak memory so badly if you donât use a polling mechanism? Consider the âtypicalâ XHR code pattern:
(function(){
var x=getXHRobject();
x.onreadystatechange=function() { // â create link from x (COM object) to anonymous function
if(x.readystate==4){ // â reference to x exists inside function scope, creating circular link.
// do something.
}
};
})();
The XmlHttpRequest object is treated in Javascript much like a DOM node. (More precisely, this problem affects all COM objects, including DOM nodes.) If you attach an onreadystatechange handler to it, youâve created a circular loop. The standard means of breaking these chains wonât work. The YUI connection lib polls the objectâs readystate until it is done, and then calls your success function. (If it times out or gets an error, it calls your failure function.) Since thereâs no onreadystatechange listener, thereâs no circular reference, and thus, no memory leak.