Cleanup for dynamically generated DOM elements in IE

Published on April 21, 2009 by Toran Billups

I was lucky enough to work alongside a very talented software developer when I landed my first .NET job back in June of 2006. And to this day, I find myself using many of the client side techniques he showed me to provide an amazing user experience for my customers.

The example found here shows this technique, but the basis of the concept is that if you have a subset of data in tabular form and your customer wants the ability to view additional information with a simple click of the mouse (and no post-back) ... you can request this data from the server and dynamically insert a few DOM elements to reflect the changes client side.

As I mentioned above, I have been using this since the day it was introduced because I enjoy building applications that have my customer asking the other developers I work with 'can you do this without a flash of the screen like this other application Toran did?' -sorry but I have to be honest! But it was not until my latest application was being user tested that I paused to think about what our fabulous IE6 browser was doing with all these dynamically generated DOM elements.

To give some background on the application and how it might be different than my previous implementations- A user of my latest app will come in and be assigned x amounts of work that will be represented in a simple html table. They will then expand each unit of work to view a level of detail to determine if they need to drill down to yet another level. If they do need to expand the next level down the amount of dynamic elements could be close to 200+

When I was working in the application for about 10 minutes, the DOM elements grew from 529 to over 2700. And as this grew so did the memory footprint of the browser. Now at first glance I thought 'this junk IE6 and the never ending memory leaks' but as it turns out IE 6/7/8 all had this very problem when I would profile the DOM elements using Drip / IE Sieve.

So as always, I post my question on stackoverflow. The first answer I got was the usual 'obvious' no help until I asked the question in more 'simple' terms.

You cannot dispose of them in such a way that when the javascript garbage collector comes around, these elements are removed from memory?

And it was this comment that got the reply I needed. The person made a comment linking to this msdn thread about 'IE appendChild/removeChild memory problem'. BINGO! The main reply in this thread that got me the results I wanted was this quote

I figured it out. I guess it is a psuedo leak it gets cleaned up on page refresh but the problem is i guess the browser for whatever reason keeps a reference to the removed child somewhere so you have to set the innerHTML of the parent to a empty string and it fixes it. no idea why it was behaving differently on different machines tho.

And so from this I found that if I altered my remove child code to the below, IE would actually remove the dynamically generated DOM elements! w00t! But this alone did not solve 100% of the issue so I took note of the 2nd answer mentioned and found that I should filter the server generated html before I append it to the DOM (instead of after as I had it previously). The main reason I had to parse the server generated html in the first place was that I typically need to use aspnet controls like a gridview or dropdownlist and if you use these they need to live inside an aspnet form tag that generates some nasty elements such as -viewstate, eventargs and another form element. But as I only want the DOM elements with detailed information, I simply parse that pre DOM append now as seen below

On success of the AJAX call I create a tr, td and div element. but instead of appending all the markup, I first call into the 'removeExtraFormData' function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function ToggleProductDetails(obj, id) {
    obj.blur();

    if (lastSelectedItem == id) {
        collapseDetails(obj);
    } else {
        if (document.getElementById('detailTR')) {
            collapseDetails(obj);
        }
        GetProductDetails(obj, id);
    }
}
function GetProductDetails(obj, id) {
    $.ajax({
        type: 'GET',
        url: 'ProductDetailsView.aspx?id=' + id,
        dataType: 'html',
        beforeSend: function() { lastSelectedItem = id; },
        error: function(XMLHttpRequest, textStatus, errorThrown) {
            alert(XMLHttpRequest.responseText);
        },
        success: function(xhtml) {
            var tr = document.createElement('tr');
            tr.id = 'detailTR';

            var td = document.createElement('td');
            td.colSpan = 3;

            var container = document.createElement('div');
            container.id = 'fillDiv';

            obj.parentNode.parentNode.parentNode.insertBefore(tr, obj.parentNode.parentNode.nextSibling);

            td.appendChild(container);
            tr.appendChild(td);

            //cleanup the html from aspnet and append the
	    //new elements to the DOM
            removeExtraFormData(xhtml, $('#fillDiv'));
        }
    });
}

The single responsibility of this function is to take the server generated markup and parse it to remove the form, viewstate and event args that are not needed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    function removeExtraFormData(xhtml, parentObj) {
      var div = document.createElement('div');
      $(div).html(xhtml);

      $(div).children().each(
            function() {
              if ($(div).find('div').filter(function() { return $(this).attr('id') == ''; }).remove());
            }
        );

      var children = $(div).find('form').children();

      $(div).find('form').remove();

      // append the correct child element back to the DOM
      parentObj.append(children);

      div = null;
    }
    

This helped reduce the amount of DOM objects being appended in the first place. Next I found if I used a more complete node removal function that went through and removed every child node in addition to using a special hack to reduce the IE memory leak issue, I got the results I was looking for.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    function collapseDetails(obj) {
        if (document.getElementById('detailTR')) {
            removeChildSafe(document.getElementById('detailTR'));
            lastSelectedItem = null;
        }
    }
    function removeChildSafe(el) {
        //before deleting el, recursively delete all of its children.
        while (el.childNodes.length > 0) {
            removeChildSafe(el.childNodes[el.childNodes.length - 1]);
        }
        el.parentNode.removeChild(el);
        discardElement(el);
    }
    function discardElement(el) {
        var bin = document.getElementById('IELeakGarbageBin');

        if (!bin) {
            bin = document.createElement('DIV');
            bin.id = 'IELeakGarbageBin';
            document.body.appendChild(bin);
        }

        bin.appendChild(el);
        bin.innerHTML = '';
    }
    

Now when I profile this application I notice the number of DOM elements gets reduced when the collapseDetails method is called!