IE innerHTML Memory Leak

Kristoffer Henriksson has got to be one of the most intelligent people I know. His ability to write, test, and debug code is second the none. Heck, I wouldn't be surprised if the guy can read and write machine code as if it were English.

Recently, Kristoffer did a significant amount of investigation to track down a nasty IE memory leak issue. IE's problems with memory leaks are well known, but most of the time, you'll hear people saying things like "closures are the cause of memory leaks," or "failing to detach events is what causes memory leaks," and (probably the most common), "circular references are the culprit -- they cause memory leaks."

To some degree, all of these things are true. Closures can sometimes lead to memory leaks, failing to detach events can also lead to memory leaks, and yes, circular references to DOM elements can also be the cause of memory leaks.

One kind of leak that seems to get a little less attention though, is the one involving .innerHTML. Fortunately (thanks to Kristoffer's investigation) this one is pretty easy to deal with.

Before we can deal with the leak, we need to understand what causes it. Three conditions are required:

  1. A orphaned DOM element must exist in memory.
  2. That element's innerHTML must be set to a string of markup the creates an element with some DOM 0 event wired up.
  3. The aforementioned innerHTML property must be set before the orphaned DOM element is "unorphaned" (added to the page).

If these conditions are met, get ready to call the (Javascript) plumber, because you've got a leak!

To make things a little less abstract, I'll write some code and explain piece by piece, how it fits the conditions above:

// 1.  An orphaned DOM element must exist in memory.
var elem = document.createElement('div');
// poor elem is just floating around in the page's memory without a parentNode

// 2.  elem's innerHTML must be set to a string of markup that creates an element with some DOM 0 event wired up.
elem.innerHTML = '<a onclick = "testFn()">Test Link</a>';
// elem contains an <a> tag with an onclick event

// 3.  elem is now added to the page, <em>after</em> innerHTML has already been set.
document.body.appendChild(elem)
// elem (and its child <a> tag) is now part of the page (remember, that innerHTML has already been set)

As you can see, the code above is not at all an uncommon scenario. Chances are though, if that's all you've got, you won't notice much of leak. This issue really only manifests itself when multiplied, that is, when you set .innerHTML in the above manner over and over and over again on the same page.

Why would ever do this? Think Web 2.0, think AJAX, think dynamic page updates and you probably won't have to search too hard to find a situation where someone might do this. Maybe you already know of (or helped write) a Web 2.0 app that does this.

Memory Leak Demo Page

Anyway, when above pattern is multiplied (whatever the reason) memory leaking chaos will ensue. To illustrate the problem, I've put together a sample page with a script that creates a orphaned <div> element, whose .innerHTML is set to a string of 2000 <a> tags with onclick events. When you click the "Start Leak" button, the script executes repeatedly, until you click the "Stop Leak" button. This give you a chance to use Perfmon to monitor memory consumption and see the results of the leak. The memory leak demo page can be found here.

The solution

It turns out, the solution is actually pretty easy: simply make sure to set .innerHTML after appending the DOM element to the page. That's it!

// create the element in memory
var elem = document.createElement('div');

// add it to the page before setting .innerHTML
document.body.appendChild(elem)

// now it's safe to set .innerHTML
elem.innerHTML = '<a onclick = "testFn()">Test Link</a>';

Of course there are other solutions, one of which is to avoid using .innerHTML at all. As a general best practice, I encourage this (for a number of reasons that I don't have time to get into here), but I recognize that that suggestion may not also be desirable, or practical.

To see that this really does take care of the issue, I've put together another test page. In this one, the .innerHTML property is only set after the DOM element has been added to the page.

If you click the "Start Leak" button on each page and let it run for about 60 seconds or so, you should notice that the first page continues to consume more and more memory as time goes on, whereas the second page's memory consumptions remains relatively constant.

Demo Links

In case you're the kind of person who doesn't like to read paragraphs to find embedded links (I'm like that), here are the links to the two demo pages, for your convenicne:

So there ya go: a nasty bug, but thanks to Kristoffer's work tracking tracking down the exact causes of this leak, a fairly easy one to handle.

Comments welcome.

12 Responses

  1. Michael Says:

    Maybe the only correct way to use innerHTML LEAK FREE http://www.posos.com/page/Index.cfm?SelNavID=2714. Use outerHTML…

  2. Piyush Says:

    Please correct me if I am wrong, but the scripts on both your 'leak' and 'noleak' pages are identical and hence no difference in performance there.

    However I copied, modified and tested locally and it is definitely true. So, thank you very much for the advise as I was experiencing the same problem with my web app and wasn't sure what the problem was.

    Moreover, it seems that is not only true for IE it is very much so for Firefox as well.

  3. Stephen Stchur Says:

    Oh, good catch! Thanks… I must have goofed when I originally uploaded the scripts. I've corrected this.

    I do not however, see the leak in Firefox as you say. Be sure you are testing Firefox in safe-mode with all add-ons disabled. Add-ons like Firebug and Greasemonkey are definitely known to make it look like Firefox itself is leaking, when in fact it is not.

  4. Matthew Kime Says:

    i'm looking at both test with IE Sieve – http://home.wanadoo.nl/jsrosman/ – and both the leak and no leak demos seem to leak just as much. can anyone verify this?

    i'm trying to get to the bottom of my own leaks – feel free to email me.

  5. Vladimir Says:

    i'm using drip http://www.outofhanwell.com/ieleak/index.php?title=Main_Page to test yours examples. All of them are leaking in ie7!
    It's critical problem for me and my company;-)
    My observation.
    If i create table row(or cell) with .insertRow(rowindex) no leak, but
    vat row=document.createElement('tr');
    tbl.appendChild(row) leak!
    My problem is i can't add any element like table row(cell), so i have to accept this. Any suggestion?

  6. sstchur Says:

    Vladimir: I have used Drip myself in the past, but to be honest, I'm very skeptical as to how accurate (or even useful for that matter) it is. I've gotten some very strange indications from it, so I take what it tells me with a grain of salt these days.

  7. Mic Says:

    Hi,
    I used your solution in a method of our javascript rendering engine ( PURE ).

    The previous code was bleeding memory on IE.
    Not anymore with your solution.

    Thanks a lot. I mean a lot.
    Cheers,

  8. sstchur Says:

    Glad it worked out for you Mic!

  9. Artsiom Says:

    Thanks a lot! I had this issue in my application. But it seems reproducible only for IE6 in my case.

  10. ondiz Says:

    I've tested you solution but leak contninue…. Memory increase and #dom increase!

  11. legends Says:

    I have tested both pages in sIEve and both leak, there is a steady increase of memory consumption in "Leak" and "NoLeak".

  12. Heng Says:

    This seems documented on MSDN website since 2005:
    http://msdn.microsoft.com/en-us/library/bb250448(v=vs.85).aspx

Got something to say?

Please note: Constructive criticism is welcome. Rude or vulgar comments however, are not and will be removed during moderation.