Hedger Wang has been scanning a lot of Chinese blogs lately for solutions to IE6 and memory leak issues. One of the things he stumbled upon is a pretty nifty way of nulling the objects to stop memory leaks by using the try ... finally construct. So instead of this solution which leaks memory:

Wow. If it works, that’s great. I have a heck of a time with IE6 (which I wish would go away, but it stubbornly persists).

Doesn’t really “solve” my problems unless the libraries I use pick it up as well, though. Hopefully all the library authors aree Ajaxian readers.

Comment by Nosredna — June 9, 2008

Aren’t the leaks typically caused by a chain of references from the event handler to the element and vice-versa. element.onblah = function() { … } references the function, and likewise, the function body eventually ends up referencing the element, so you have a cycle. I don’t see how nulling the local var is going to stop memory leaks.

I know, I was just emphasizing that it had Crockford’s leak case, a case that developers are likely to be familiar with.

Comment by Nosredna — June 9, 2008

“nulls are cleaned up by garbage collection.”

Well, that would be an amazing new definition of garbage collection for me. If an object is unreachable from all program roots, then yes, it should be collected. The problem has always been that IE uses a reference counting scheme for GC, and reference counting schemes are vulnerable to cycles. Creating an event handler which holds a reference to the element it is attached creates a cycle, such that even when no external references exists, the reference count is non-zero. IE tries to fix this, by disposing of all the attached DOM elements on unload, but this fails if you create or detach DOM elements and put event handlers on them.

I’m not saying the posted code doesn’t work (I’d like to see Drip or some other tool/test confirm it), but the question is *why* should it work. If I have a closure handling onclick which “captures” the element it is attached to, then there is a cycle, regardless of how many local variables are aliased to the element and/or nulled out.

Moreover, the example givens hows that the element is being returned from the function, so in no way is it safe to GC the created “button” element and closure, since in all likelyhood, that return value might be assigned somewhere else.

If this hack works, it’s an uber hack, because it’s leveraging some weird bugs/semantics of IE6’s GC.

Ok, the hedgerow link shows an example that perhaps won’t leak, but it’s doing something different than the proposed solution above. It creates an event handler that essentially ignores all of the DOM element’s information, except for an expando reference. It then nulls out the reference to the element. Cool and surprising that it works, but it doesn’t solve the usecase that the majority of us want.

Try changing the event handler to reference .innerHTML of the element, or call getAttribute() and see if it still doesn’t leak.

I played around with this finally approach tested it in sIEve and it does really seem to solve the circular reference memory leak. Very strange that it works. However it doesn’t directly solve the circular reference problem that the attachEvent method in IE introduces so I wrote a small proof of concept class that fakes this behavior.

One way to avoid circular reference related DOM memory leaks in IE6 is to institute a CRIS (Circular Reference Isolation System). Your framework should provide methods for abstracting the assignment and removal of event handlers on DOM nodes. With such an abstraction, you can institute a CRIS to enforce a breakage in the circular reference between referenced DOM nodes and lambda closures that would implicitly be linked by reference to the DOM nodes.

Basically, the DOM node has a reference to the closure function (the event handler). The closure function has a reference to the state of its enclosing scope, which has a variable that is a reference to the DOM node. IE6 doesn’t identify and resolve circular references that span the JS/DOM divide.

A CRIS works by assigning an intermediary “joiner” function as the actual event handler of the DOM node, and ensures that this intermediary function does not have a direct reference to the handler function being assigned by the application. Instead, the real handler is looked up from a hash table, using a key that is stored through closure with intermediary. This effectively breaks the direct reference chain, so IE6 doesn’t hang on to DOM nodes. To the application developer, everything behaves the way they would like. They can assigned closure functions as event handlers without worrying about circular references.

PLUG (of course, you knew it was coming): The UIZE JavaScript Framework employs a CRIS in its Uize.Node package, so that the event handler methods ensure no circular references that would lead to DOM memory leaks in IE6.

I’ve talked to people who have implemented CRIS style stuff before, it’s a fairly obvious workaround, but AFAIK, it doesn’t work perfectly either. The GC shouldn’t care whether references are direct or indirect with respect to cycles. If an element has a reference to another JS object, which references another, which eventually references the original element, you have a cycle. There’s nothing special about A->B->A vs A->B->C->A. It shouldn’t matter if C is a direct reference, or a hash table with a bunch of references, one of whom happens to be A.

AFAIK, no framework that doesn’t track unattached DOM elements and clean them up on page load (or unattachment) can avoid leaking.

>>We need to make sure that the people USING IE6 know that it is garbage.

They often do know it. Many of them work at places where IT has mandated IE6. These are often oppressive places to work, and telling the user that the browser sucks is just adding to the punishment they already endure.

You have to decide if it’s worth dealing with IE6 or not. A local newspaper’s site stopped working on IE6 recently. The programmers get the complain calls forwarded to them (something like 20% of the users had been on IE6 still), and they have to explain on the phone that IE6 is no longer supported. That seems like a great way to alienate your customers to me.

I’d LOVE it if IE6 went away. It uss up probably half my browser-specific time. It’ll be a big win when I can drop it.

Comment by Nosredna — June 9, 2008

Cromwellian’s comments (hi, Ray!) are pretty much right on the money. No amount of indirection through hash tables or any other mechanism will change the fact that you’ve got a circular reference. Think about it this way: If I’ve got a Javascript object that references a DOM element, and it’s possible to reach that object *in any way* from the DOM element (including its event handlers), then you’ve got an uncollectible circular reference in IE. This can only be fixed by breaking the circular reference at some point before the page unloads, and once you do so, whatever was depending on that reference (e.g. being able to handle events correctly) will no longer work.

The trick described in this article works for the sole reason that the only circular reference involved is { functionScope -> element -> eventHandler -> closure -> functionScope }. The finally block just clears out the { functionScope -> element } part of the reference after the return value is computed. This in no way solves the general problem described above. The only general solution is to break circular references when an element is “disposed” (i.e. whenever it is no longer needed by the app, and before the page unloads).

The trick is to break the *reference* chain. If the event handler that is directly assigned to the DOM node has a string variable (closure scoped) that contains a key which it can use to look up a true handler in a hash table, then the handler does *not* have a reference to the true handler function. Therefore, the DOM node does not have a reference to the true handler function. In all likelihood the true handler function hangs on to a reference to the DOM node, but this is OK because there is now no circular reference taking place.

It was a long time ago that I solved this problem and made a test to verify that it worked. I was using a little utlility called Drip at the time that indicated that with the CRIS in place, there were no leaked DOM nodes across page refreshes. Needless to say, it was a relief to solve this problem in an elegant way. Prior to this, UIZE had a workaround for IE6 where cleanup would happen on page unload (the traditional way). I remember that the key thing was to keep the direct handler function definition outside of a scope which held a closure reference to the hash table.

The problem with that approach is that you now have the following reference chain: { handlerFunction -> hashTable -> trueHandlerFunction -> element }. If the hashTable is simply a global, then the handlerFunction still has an implicit reference to it (by virtue of the fact that everyone has an implicit reference to the global scope). This is generally not a big deal with a simple handler function, but if that handler has a reference to a more complex “peer” javascript object (very common in UI frameworks), then the leak can be much larger.

It is possible that Drip is showing that you have no leaks, because IE (7, at least) cleans up event handlers it can find in the DOM on unload. Drip can only check for leaks present *after* the page is fully done unloading (I wrote Drip a few years ago, but haven’t really maintained it much since).

Attaching “any” JS objects to a DOM element would introduce memory leak, and that’s why add expando to an element is a bad idea.

After M$ had released several fixes to fix the IE memory leaks bug, it turns out that IE does remove all JS objects attached to DOM elements before the unload event is triggered as long as those DOM elements remain valid in the document tree.

For element removed or replaced (eg.document.body.innerHTML = “”) from the document tree before the unload event, IE won’t be able to recycle the memory from the JS objects attached to those elements.

It’s not just expandos tho. If you use .onXXX, or attachEvent, the same leak will occur. It’s any reachable reference from the DOM node into the JS which causes the issue for removed/replaced elements.

@jgw
Drip was used to test leaking in IE6 – not IE7. Also the hash table is not a global variable, but stored as a property of a global object. That particular pattern, at the time, resulted in Drip reporting no leaked DOM elements, and the memory usage of IE6 not creeping up uncontrollably on each subsequent page refresh.