Sunday, July 01, 2007

IE Memory Leaks

Mark Wubben blogged that Microsoft recently released a security patch that among other things fixed memory leaks in IE 6.0 on XP. Specifically, Microsoft mentions a fix for --

"A memory leak occurs in Internet Explorer 6 when you view a Web page that uses JScript scripting on a Windows XP-based computer"

More details are found here.

It's important to note that the fix is only for IE 6.0 running in XP. It doesn't address IE 6.0 running in W2K or IE 5.5. It's also a patch that's applied to the entire OS with a number of other fixes. So, some people ( ex. corporations ) may be hesitant to install it until they've fully verified that the patch is more beneficial than hurtful.

Of course, if you're using IE 7, none of this matters because memory leaks don't occur there anymore.

With these elements in play, I'd thought we'd review memory management in IE.

Memory leaks occur in IE because of circular references between DOM elements and JavaScript objects ( it's really JScript objects -- Microsoft's implementation of JavaScript ).

Circular references aren't a problem with other browsers just IE. This is because IE uses two different memory managers -- one for DOM elements ( DOM elements are represented as COM objects and so use the COM garbage collector ) and one for JavaScript objects which uses a different one.

Since the two memory managers don't communicate with one another they don't know that there are "islands" of circular references which are no longer used. The memory managers get confused and then bad things happen.

Note that I've simplified this a lot. If you want the gory details read Joel Webber's dated but really good description of the issue.

In IE, when memory leaks happen, they don't go away when you navigate to a new page. Instead, the memory loss carries over. If you use a process/memory viewer like Process Explorer, you'll observe this.

The only way to free the memory ( other than breaking the circular references ) is to close and restart the browser.

Circular references between DOM and JavaScript objects are common. If you've ever attached an event to a DOM element and have your event handler refer back to the element, you've created a circular reference. They can also occur when you create closures ( inner functions can access variables outside of itself even after the enclosing function has returned ), but the more common occurrence is the first one.

Fortunately, it's easy to break the circular reference and as it turns out, it's easier to break the link from the DOM rather than from the JavaScript side of things.

For events, you do it differently depending on how you've attached the events.

DOM Level 0 Events
All browsers support Level 0 event handling. This is probably the first type of event handling that you learned when learning about HTML and JavaScript events. Level 0 event handling allows you to attach events directly to the HTML element like this --

<div id="SomeDiv" onclick="handleClick();"></div>

The div element has a reference to the JavaScript object handleClick via the onclick attribute. If the method handleClick has a reference back to the div ( for example "this" ), then a circular reference occurs.

It's not optimal to review every event handler to see if a circular reference exists. It's better to just unhook the event handler when you no longer need it.

So, follow this maxim of symmetry - if you add it, remove it.

For DOM Level 0 events, removing it is pretty easy. You nullify it --

<script type="text/javascript">
var someDiv = document.getElementById("SomeDiv");

someDiv.onclick = null; // Set the handler to null


DOM Level 2 Events
DOM Level 2 event handling supports a number of methods such as addEventListener and removeEventListener. The equivalent methods in IE are attachEvent and detachEvent respectively. We follow the same symmetry with Level 2 events. We add events like this ( with the IE methods ) --

obj.attachEvent( 'onclick', handleClick );

then, we do this to remove them --

obj.detachEvent('onclick', handleClick);

A generic way to do this is to use a Singleton object that abstracts how to add or remove events so you don't have to worry about the browser you're using --

It's best to cleanup the events when you "unload" from a page. Listen for the unload event and when that occurs, have your event handler remove the event handlers.

Here's a DOM Level 0 example and here's a DOM Level 2 example for you to try.

I hope that this is something you can use in your own code.

Have fun!


Abhishek said...

Nice post. Thanks!
If the innerHTML of a div is set to "", would all the onclick's set on the children elements be removed automatically?

skypoet said...

Hi abhishek,

Unfortunately, setting the innerHTML to "" won't clear out all the DOM 0 event handlers ( i.e. onclick ).

So, you have to keep track of all the DOM 0 events and then "null" them out.

This is why it's almost always better to use DOM 2 event handler abstracted through some library like YUI, Dojo, etc. This way you can track and remove then more easily.


Abhishek said...

Thanks Oliver. Really appreciate your help.

gil adino said...

try it and it's not working,
check both level 0 and level 2
i'm running xp sp3 and ie7 7.0.5730

i check your files on my comp this is the results

test mem vm total
open browser blank page : 25,604.00 17,432.00 43,036.00
load test page : 29,368.00 18,052.00 47,420.00
click start (10000 divs): 45,016.00 33,328.00 78,344.00
refresh : 37,572.00 25,504.00 63,076.00 cpu t0 50% mem to 110mb and vm to125mb in the process
click start (10000 divs): 48,408.00 36,324.00 84,732.00
refresh : 51,396.00 39,416.00 90,812.00 cpu t0 50% mem to 120mb and vm to125mb in the process
minimize browser : 1,720.00 39,416.00 41,136.00
restore browser : 6,340.00 39,416.00 45,756.00

test mem vm total mem
open browser blank page 25,452.00 17,244.00 42,696.00
load test page 29,328.00 17,968.00 47,296.00
click start (10000 divs) 34,620.00 22,840.00 57,460.00
refresh 48,628.00 36,860.00 85,488.00 cpu t0 50% mem to 100mb in the process
click start (10000 divs) 45,640.00 33,812.00 79,452.00
refresh 51,482.00 39,564.00 91,046.00 cpu t0 50% mem to 100mb in the process

you can see that the only way to free memory ,that i found, is to minimize the browser window, and it's not working on the vm.