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.

7 Comments

  1. Two things I’m curious about:

    1) Are you aware of any differences on how IE7 handles garbage collection?

    2) How do other browsers handle circular links?

  2. 1. IE7 does not leak memory in the same way that IE6 does.

    2. Great question. It’s funny you ask, since I’m actually researching and working on a blog post that will discuss this, but here’s a quicky $0.50 tour of the landscape.

    The problem is not that the links are circular, per se, but that the references make a circle between Dom (really, XPCom Com) object-space and Javascript object-space. IE6 handles circular links fine as long as they never cross that boundary.

    I’m not sure of the specifics of every implementation of course, but conceptually, ECMAScript suggests a “mark and sweep” method of garbage collection. Each object has a “mark” bit. Objects fall into two categories: globals (or “roots”) and scope variables. Every “so often” (on a timer, or when some condition is met, or at particular points in the script parse/execute cycle, or whatever), the garbage collector runs through all the objects in memory, and clears the mark bit. Then, it starts from the root global objects and the scoped objects on the call stack, and traverses the tree, marking every variable that it can reach. (Note, this also includes the call-stacks of all function objects that are reachable—that’s part of the reason why functions are so big, memory-wise.)

    Once it’s traversed the tree of objects and marked the things that are reachable, it “sweeps” through again, and releases any objects that are not marked back into the heap.

    So, if object A refers to object B, and object B refers to object A, but neither of them are reachable from the outside, then they’ll both be reclaimed when the garbage collector sweeps through.

    VBScript and XPCom Com (used to) use much simpler garbage collection mechanisms involving reference counting. So, when an XPCom Com object (like a Dom node) refers to a Javascript object that refers back into XPCom Com space, you have a case where objects A and B both have a single reference to them, and they’ll never be deleted. I’m not sure how Microsoft fixed this with IE7, but I’m very happy that they did. It’s about time. I can’t wait for this blog post to be outdated :)

  3. Isaac: Seeing as you’re talking about *Microsoft’s* browser, I assume that when you say “XPCom”, you actually just mean “COM”. “XPCom” is Mozilla’s cross-platform invention - “COM” is Microsoft’s technology.

  4. @Chris

    Ha. You’re absolutely right *^_^*

    Editing now, thanks for the correction.

  5. http://chris-forbes.livejournal.com/2008/01/22/ might shed some light on *why* these memory leaks happen - I was going to post it right here, but it was article-length anyway ;)

    Hope you don’t mind, Isaac :)

  6. @Chris

    I don’t mind at all.

    Like I said, hopefully this entire topic will be one for the history books sooner rather than later. IE 7 is gobbling up browser share on PCs pretty quickly. On Wikipedia, IE 7 is up to 40%, and IE 6 is down to 35%. Last I heard at Yahoo, IE 7 was about tied with IE 6, and that was only 6 months after release. IE 7 isn’t my favorite browser to develop for, but at least it doesn’t leak memory as badly.

  7. I’m using the YUI Connection library and Drip reports that my app is clean of leaks, but IE6 still leaks memory over time with my app. What else should I try?

Post a Comment

Post Friendly.