Mottie
Mottie

Reputation: 86473

Cleaning up memory leaks

I am using jQuery to clone elements, then I save a reference to an element within that clone. And much later remove the clone. Here is a basic example:

HTML

<div> <span></span> </div>

Script

var i, $clone, $span,
    $saved = $('span'),
    $orig = $('div');

for (i = 0; i < 100; i++) {
    $clone = $orig.clone().appendTo('body');
    $span = $clone.find('span');

    $saved = $saved.add($span);
    $clone.remove();
}
console.log( 'leaking = ', $saved.length);

The console log outputs a length of 101.

I need to clean up the $saved jQuery object and remove references to elements no longer attached to the DOM. So I wrote this basic function to clean it all up.

var cleanUpLeaks = function ($el) {
    var el, remove,
    index = $el.length - 1;
    while (index >= 0) {
        el = $el[index];
        remove = true;
        while (el) {
            el = el.parentNode;
            if (el && el.nodeName === 'HTML') {
                remove = false;
                break;
            }
        }
        if (remove) {
            $el.splice(index, 1);
        }
        index--;
    }
    return $el;
};

console.log( 'cleaned up = ', cleanUpLeaks( $saved ).length );

This time the console outputs 1.

So now my questions are:

Demo: http://jsfiddle.net/Mottie/6q2hjazg/


To elaborate, I save a reference to the span in $saved. There are other functions that use this value for styling and such. This is a very basic example; and no, I do not immediately remove the clone after appending it to the body, it was done here to show how the memory leak is occurring.

Upvotes: 1

Views: 525

Answers (2)

jfriend00
jfriend00

Reputation: 708056

The better solution here is to stop saving dynamic DOM elements in a persistent jQuery variable. If your page is regularly removing content from the DOM, then saving these in a persistent jQuery object just sets you up for having to deal with memory leaks, rather than changing the design to a design that does not have to save references to DOM elements at all.

If instead, you just tag interesting elements with a particular class name that is not used elsewhere in the document, you can generate the desired list of elements at any time with a simple jQuery selector query and you will have no issues at all with leaks because you aren't ever retaining DOM references in persistent variables.

Upvotes: 1

GregL
GregL

Reputation: 38151

One possible solution is that you take a leaf out of AngularJS's book and monkey-patch jQuery to fire an event when an element is removed. Then you can add a handler for that event and restore the state of $saved to what it was before you added the $span.

First, monkey patch jQuery (taken from AngularJS source):

// All nodes removed from the DOM via various jQuery APIs like .remove()
// are passed through jQuery.cleanData. Monkey-patch this method to fire
// the $destroy event on all removed nodes.
var originalCleanData = jQuery.cleanData;
var skipDestroyOnNextJQueryCleanData;
jQuery.cleanData = function (elems) {
    var events;
    if (!skipDestroyOnNextJQueryCleanData) {
        for (var i = 0, elem;
        (elem = elems[i]) != null; i++) {
            events = jQuery._data(elem, "events");
            if (events && events.$destroy) {
                jQuery(elem).triggerHandler('$destroy');
            }
        }
    } else {
        skipDestroyOnNextJQueryCleanData = false;
    }
    originalCleanData(elems);
};

Next, add in your $destroy event handler and restore the captured original state of $saved.

var i, $clone, $span,
    $saved = $('span'),
    $orig = $('div');

for (i = 0; i < 100; i++) {
    (function ($originalSaved) {
        $clone = $orig.clone().appendTo('body');
        $span = $clone.find('span');

        $clone.on('$destroy', function () {
            $saved = $originalSaved;
            $originalSaved = null;
        });
        $saved = $saved.add($span);

        $clone.remove();
    })($saved);
}
console.log('original length = ', $saved.length); // => 1

Here is a jsFiddle with this working. In my testing in Chrome, this doesn't introduce additional leaks.

Upvotes: 1

Related Questions