jQuery .remove() vs Node.removeChild() and external maps to DOM nodes - javascript

The jQuery API documentation for jQuery .remove() mentions:
In addition to the elements themselves, all bound events and jQuery
data associated with the elements are removed.
I assume "bound events" here means "event handlers"; documentation for the similar .empty() says:
To avoid memory leaks, jQuery removes other constructs such as data
and event handlers from the child elements before removing the
elements themselves.
It does sound like leaks would ensue if one were to not use these functions and use Node.removeChild() (or ChildNode.remove()) instead.
Is this true for modern browsers?
If so, why exactly can't properties and event handlers be collected once the node is removed?
If not, do we still need to use .data()? Is it only good to retrieve HTML5 data- attributes?
Documentation for jQuery.data() (lower-level function) says:
The jQuery.data() method allows us to attach data of any type to DOM
elements in a way that is safe from circular references and therefore
free from memory leaks. jQuery ensures that the data is removed when
DOM elements are removed via jQuery methods, and when the user leaves
the page.
This sounds an awful lot like a solution to the old IE DOM/JS circular leak pattern which, AFAIK, is solved in all browsers today.
However, a comment in the jQuery src/data.js code (snapshot) says:
Provide a clear path for implementation upgrade to WeakMap in 2014
Which suggests that the idea of storing data strictly associated to a DOM node outside of the DOM using a separate data store with a map is still considered in the future.
Is this just for backward-compatibility, or is there more to it?
Answers provided to other questions like this one also seem to imply that the sole reason for an external map is to avoid cyclic refs between DOM objects and JS objects, which I consider irrelevant in the context of this question (unless I'm mistaken).
Furthermore, I've seen plugins that now set properties on relevant DOM nodes directly (e.g. selectize.js) and it doesn't seem to bother anyone. Is this an OK practice? It certainly looks that way, as it makes removing entire DOM trees very easy. No need to walk it down, no need to clean up any external data store, just detach it from the parent node, lose the reference, and let the garbage collector do its thing.
Further notes, context and rationale to the question:
This kind of capability is especially interesting for frameworks that manage views (e.g. Durandal), which often times have to replace entire trees that represent said views in their architecture. While most of them certainly support jQuery explicitly, this solution does not scale at all. Every component that uses a similar data store must also be cleaned up. In the case of Durandal, it seems they (at least in one occurrence, the dialog plugin - snapshot) rely on Knockout's .removeNode() (snapshot) utility function, which in turn uses jQuery's internal cleanData(). That's, IMHO, a prime example of horrible special-casing (I'm not sure it even works as it is now if jQuery is used in noConflict mode, which it is in most AMD setups).
This is why I'd love to know if I can safely ignore all of this or if we'll have to wait for Web Components in order to regain our long-lost sanity.

"It does sound like leaks would ensue if one were to not use these functions and use Node.removeChild() (or ChildNode.remove()) instead.
Is this true for modern browsers?
If so, why exactly can't properties and event handlers be collected once the node is removed?"
Absolutely. The data (including event handlers) associated with an element is held in a global object held at jQuery.cache, and is removed via a serial number jQuery puts on the element.
When it comes time for jQuery to remove an element, it grabs the serial number, looks up the entry in jQuery.cache, manually deletes the data, and then removes the element.
Destroy the element without jQuery, you destroy the serial number and the only association to the element's entry in the cache. The garbage collector has no knowledge of what the jQuery.cache object is for, and so it can't garbage collect entries for nodes that were removed. It just sees it as a strong reference to data that may be used in the future.
While this was a useful approach for old browsers like IE6 and IE7 that had serious problems with memory leaks, modern implements have excellent garbage collectors that reliably find things like circular references between JavaScript and the DOM. You can have some pretty nasty circular references via object properties and closures, and the GC will find them, so it's really not such a worry with those browsers.
However, since jQuery holds element data in the manner it does, we now have to be very careful when using jQuery to avoid jQuery-based leaks. This means never use native methods to remove elements. Always use jQuery methods so that jQuery can perform its mandatory data cleanup.
"Furthermore, I've seen plugins that now set properties on relevant DOM nodes directly (e.g. selectize.js) and it doesn't seem to bother anyone. Is this an OK practice?"
I think it is for the most part. If the data is just primitive data types, then there's no opportunity for any sort of circular references that could happen with functions and objects. And again, even if there are circular references, modern browsers handle this nicely. Old browsers (especially IE), not so much.
"This is why I'd love to know if I can safely ignore all of this or if we'll have to wait for Web Components in order to regain our long-lost sanity."
We can't ignore the need to use jQuery specific methods when destroying nodes. Your point about external frameworks is a good one. If they're not built specifically with jQuery in mind, there can be problems.
You mention jQuery's $.noConflict, which is another good point. This easily allows other frameworks/libraries to "safely" be loaded, which may overwrite the global $. This opens the door to leaks IMO.
AFAIK, $.noConflict also enables one to load multiple versions of jQuery. I don't know if there are separate caches, but I would assume so. If that's the case, I would imagine we'd have the same issues.
If jQuery is indeed going to use WeakMaps in the future as the comment you quoted suggests, that will be a good thing and a sensible move. It'll only help in browsers that support WeakMaps, but it's better than nothing.
"If not, do we still need to use .data()? Is it only good to retrieve HTML5 data- attributes?"
Just wanted to address the second question. Some people think .data() should always be used for HTML5 data- attributes. I don't because using .data() for that will import the data into jQuery.cache, so there's more memory to potentially leak.
I can see it perhaps in some narrow cases, but not for most data. Even with no leaks, there's no need to have most data- stored in two places. It increases memory usage with no benefit. Just use .attr() for most simple data stored as data- attributes.

In order to provide some of its features, jQuery has its own storage for some things. For example, if you do
$(elem).data("greeting", "hello");
Then, jQuery, will store the key "greeting" and the data "hello" on its own object (not on the DOM object). If you then use .removeChild(elem) to remove that element from the DOM and there are no other references to it, then that DOM element will be freed by the GC, but the data that you stored with .data() will not. This is a memory leak as the data is now orphaned forever (while you're on that web page).
If you use:
$(elem).remove();
or:
$(some parent selector).empty()
Then, jQuery will not only remove the DOM elements, but also clean up its extra shadow data that it keeps on items.
In addition to .data(), jQuery also keeps some info on event handlers that are installed which allows it to perform operations that the DOM by itself can't do such as $(elem).off(). That data also will leak if you don't dispose of an object using jQuery methods.
In a touch of irony, the reason jQuery doesn't store data as properties on the DOM elements themselves (and uses this parallel storage) is because there are circumstances where storing certain types of data on the DOM elements can itself lead to memory leaks.
As for the consequences of all this, most of the time it is a negligible issue because it's a few bytes of data that is recovered by the browser as soon as the user navigates to a new page.
The kinds of things that could make it become material are:
If you have a very dynamic web page that is constantly creating and removing DOM elements thousands of times and using jQuery features on those objects that store side data (jQuery event handlers, .data() on those elements, then any memory leak per operation could add up over time and become material.
If you have a very long running web page (e.g. a single page app) that stays on screen for very long periods of time and thus over time the memory leaks could accumulate.

Related

Options to link DOM node to (in-browser) domain object: is direct reference OK?

For a single-page app: I want each of my DOM nodes to have a reference to a single (in-browser) domain object. Is it OK to just store a direct reference like this:
var myDomainObject = ...;
var DOMNode = document.getElementById("myId");
DOMNode.domain_object = myDomainObject;
Is this safe, repeatable? Can the browser do mysterious things with added-on JavaScript properties?
Thanks.
From an experiential standpoint: I've attached data directly to nodes without issue and never had a problem. From a specification standpoint, my interpretation is that doing so is not necessarily recommended, but is safe to the extent that the custom attributes don't conflict anything else.
In Common Infrastructure - Extensibility, the recommendation for authors (that's you) is to use only [data-*] attributes:
Authors can include data for inline client-side scripts or server-side
site-wide scripts to process using the data-*="" attributes. These are
guaranteed to never be touched by browsers, and allow scripts to
include data on HTML elements that scripts can then look for and
process.
And the requirement for a valid user-agent is to leave anything in the DOM it doesn't recognize.
User agents must treat elements and attributes that they do not
understand as semantically neutral; leaving them in the DOM (for DOM
processors), and styling them according to CSS (for CSS processors),
but not inferring any meaning from them.
So, my suggestion, along the same line as the W3C's aim of avoiding conflicts, is to create objects that refer to DOM elements. But, if you "must" tag weird things onto the DOM, rest assured that user agents are required to leave it there. But if you really must, it may be wise to use those data-* attributes!
(I personally don't use them and tend to slap objects and values onto whatever's most convenient at the time. But, I may be jaded by about 15 years of hacks and "feature detection" for the non-compliance of the user agents. Even now, I don't think IE supports the data-* standard ... )
I've done it before to store custom events and it works fine in every browser I tried but yeah, it's dangerous I'd say, only if there are no other alternatives and you must pass this info with the element.
At least, ideally, create your own namespace to not pollute the already polluted object:
var myDomainObject = ...;
var DOMNode = document.getElementById("myId");
DOMNode.My = {};
DOMNode.My.domain_object = myDomainObject;
Edit: Just wanted to see how many methods and properties a regular div might have, and it has 136 (in Chrome). http://jsbin.com/abecaq/1/edit
While it wouldn't cause any issues on a small site, I would avoid doing this for a few reasons...
You are opening yourself up to memory leaks by linking references to many different JS objects
Any new DOM nodes would have to have this link attached
I would recommend utilizing a global variable or ideally a variable at the highest necessary scope that can be referenced whenever necessary.

Identify javascript closures with developer tools

I am currently developing a website that is pure javascript and relies heavily on the jQuery & jQuery UI libraries (this site is not intended for use by a general public, hence progressive enhancement is not a strict requirement for this project). I am encountering a significant memory leak on executing the following code:
oDialogBox = $("<div>...</div>");
/* Add useful things to the dialog box here */
oDialogBox.appendTo("body");
oDialogBox.dialog({
/* Other dialog box settings here */
close: function(event, ui) {
oDialogBox.dialog("destroy");
oDialogBox.remove();
oDialogBox = null;
}
});
At any given time in this dialog box, I am creating, removing and modifying a large number of instances of jQuery UI buttons, multiselects (per the Multiselect widget created by Eric Hynds) and on click event handlers. According to jQuery UI documentation, calling .remove() on oDialogBox should result in all child widgets being unbound and deleted. Yet my detached DOM tree shows a significant number of garbage elements that the GC isn't collecting.
It is highly likely I have missed a large set of closures that need to be finished off safely. How do I do the following:
1) How do I identify which closures are keeping a given detached DOM object alive (either in Firefox or Chrome)?
2) Assuming the complete set of closures is identified, does anything beyond nulling the variable need to be done to assure marking the DOM element for garbage collection?
3) I have also noticed my list of arrays stored by the page is giant and contains references to DOM elements not being gathered by the GC. Is there a documented best practice for cleaning arrays from javascript and allowing all elements to be marked for deletion? (Note: this is a current prime suspect for the source of the memory leak)
I'm afraid that I don't have a great answer for #1. I haven't found any really good tools for this myself, even given how good the development tools have become over the last few years. The best advice I can give is to always keep things in the smallest scope you possibly can. If things don't escape, it's generally easier to simply figure out where the references must be.
As to #2, there can be further concerns. If the object referenced by variable v1 closes over the free variables of some function, removing v1 will not be enough to make them eligible for garbage collection if another variable v2 closes over v1 in some other function. So I guess if you really mean the "complete set of closures", then you should be all set. But this might get hairy. Again, if most object have references only in narrow scopes, these problems are much less severe.
For #3, what sorts of arrays are you discussing? If it's jQuery collections, then perhaps you simply have too many of them around. The only reason I know for them to stay around for a long time is to bind event handlers to them, and that is almost always better handled by event delegation on parent elements. If it's you're own custom arrays, do you really have a good reason to store references to them in arrays that last for any substantial length of time? I've rarely found one.

Why is it risky to store data as an attribute of an element?

I keep reading the same thing:
"Storing property values directly on DOM elements is risky because of possible memory leaks."
But can someone explain these risks in more detail?
(By attribute, I assume you are referring to properties on DOM elements.)
Are custom properties on DOM elements safe?
Some browsers have not cleaned up DOM elements very well when destroyed. References to other elements, the same element, or large sets of data were therefore retained, causing leaks. I believe this is largely resolved in newer browsers.
In any case, storing small amounts of data on an element is innocuous, and can be very convenient, so take that warning with a grain of salt.
Is using jQuery's .data() a safe alternative?
Not especially. Storing data using jQuery's custom data store has its own potential for memory leaks, and unfortunately they don't merely affect old browsers.
In order to avoid leaks, you'd need to be absolutely certain you clean an element's .data() when destroying an element. This is automatic when you use jQuery to destroy the element, but if you don't, you'll have memory leaks that affect every browser.
What are some examples that can cause leaks?
Let's say that there's a bunch of .data() linked to the #foo element. If we use jQuery methods to remove the element, we're safe:
$("#foo").remove(); // associated .data() will be cleaned automatically
But if we do this, we have a cross-browser compatible leak:
var foo = document.getElementById("foo");
foo.parentNode.removeChild(foo);
Or if #foo is a descendant of some other element whose content is being cleared without jQuery, it would be the same issue.
otherElement.innerHTML = "";
In both cases, jQuery was not used to remove #foo, so its .data() is permanently disassociated from the element, and our application has a leak.
So if I never use the DOM API directly, I'm safe?
You're safer, but another way this can happen is if we load more than one DOM manipulation library. Consider that jQuery helps us do this with the following code:
var $jq = jQuery.noConflict();
Now we can allow $ to refer to prototypejs or mootools, and jQuery is referenced by $jq.
The trouble is that those other libraries will not clean up data that was set by jQuery, because they don't know about it.
So if jQuery has some data on #foo, and mootools is used to destroy that element, we have our memory leak.
What if I never use .data() in jQuery? Does that make me safe?
Sadly, no. jQuery uses the same .data() mechanism to store other data, like event handlers. Therefore even if you never make a call to .data() to associate some custom data with an element, you can still have memory leaks caused by the examples above.
Most of the time you may not notice the leaks, but depending on the nature of the code, they can eventually grow large enough to be a problem.
According to the jQuery documentation:
In Internet Explorer prior to version 9, using .prop() to set a DOM
element property to anything other than a simple primitive value
(number, string, or boolean) can cause memory leaks if the property is
not removed (using .removeProp()) before the DOM element is removed
from the document. To safely set values on DOM objects without memory
leaks, use .data().

Can jQuery.data cause a memory leak?

Would the following piece of code create a memory leak.
According to the jQuery documentation use of the data function avoids memory leaks. It would be useful to confirm whether the following is safe.
var MyClass = function(el) {
// Store reference of element in object.
this.element = $(el);
};
// Store reference of object in element.
$('#something').data('obj', new MyClass('#something'));
Obviously the code as it stands would take up extra memory as long as the DOM element is still connected to the DOM. But I'm guessing you're asking whether it would continue using extra memory after the DOM element is no longer in use.
Update: Thanks to Joey's answer (which he has since deleted), I spent some time reading up on memory leaks in javascript, and it appears my assumptions in the paragraph below are incorrect. Because DOM elements don't use pure garbage collection, a circular reference like this would normally prevent both the DOM element and the javascript object from ever being released. However, I believe the remainder of this answer is still correct.
Without a deep knowledge of how javascript engines implement garbage collection, I can't speak authoritatively on the topic. However, my general understanding of garbage collection makes me think that your code would be "safe" in the sense that after the #something element is removed from the DOM, the resulting MyClass object would only have a reference to an object that has no other connections. The graph algorithms of the garbage collector should be able to identify that the DOM element and its MyClass object are "floating in space" and unconnected to everything else.
Furthermore, jQuery goes out of its way to strip data and events that are associated with a given DOM element once it is removed from the DOM. From the documentation:
jQuery ensures that the data is removed when DOM elements are removed via jQuery methods, and when the user leaves the page.
So assuming you use jQuery consistently, you would only have a one-way reference once the object is removed from the DOM anyway, which makes it that much easier possible for the garbage collector to know it can get rid of these objects.
So as long as you don't have something else referencing the MyClass object once the DOM element is removed, you shouldn't have a memory leak.
I suppose it depends on the Javascritp engine.
You have have the question precisely enought to perform a test. I added a long string in the object and ran the potential leak in a large loop.
As a result, I don't think in leaks in IE8 nor in Chrome.
But I could not reproduce these leakeage patterns either.
This can lead to a memory leak.
the theory of jQuery.data method may use A Data inner class to cache data for the dom element.
of course,when you remove the cache data,jQuery will unreference the data.
but the inner cache is a increasing array,when you you it ,it will go upon.
so ,in the end,there will be very big cache array,which will lead memeory leak.
In a long run web app,this may leak memory crash.
The data attribute only stores string values.

What sort of memory leaks should I watch for with jQuery's data()?

Should I pair every data() call with a later removeData() call?
My assumptions: jQuery's remove() will remove elements from the DOM, and if I don't have any other references to remove, I don't have to do any more clean up.
However, if I have some javascript var or object referring to one of the elements being removed, I'll need to clean that up, and I'm assuming that applies to jQuery's data function, too, because it's referencing the elements somehow.
So if I do need to call removeData before remove, is there a shortcut to remove all data associated with an element or do I have to call each explicitly by name?
Edit: I looked through the source code and confirmed what Borgar and roosteronacid said. Remove takes the elements out of the dom and deletes any events and data stored with them - which is convenient, but makes me wonder when you would use removeData(). Probably not often.
jQuery's data does not keep a reference to the element so that you don't need to worry about memory leaks. Its intended purpose is to solve this exact problem.
A slight simplification of how it works:
An id member is added to each "touched" DOM node. All subsequent actions involving that DOM element use that id.
var theNode = document.getElementById('examplenode');
theNode[ 'jQuery' + timestamp ] = someInternalNodeID;
You can access the id using the same function jQuery uses:
someInternalID = jQuery.data( document.body );
When you append data to the node it stores that on the jQuery object, filed under the node's internal id. Your $(element).data(key,value) translates internally to something like:
jQuery.cache[ someInternalNodeID ][ theKey ] = theValue;
Everything goes into the same structure, including event handlers:
jQuery.cache[ someInternalNodeID ][ 'events' ][ 'click' ] = theHandler;
When an element is removed, jQuery can therefore throw away all the data (and the event handlers) with one simple operation:
delete jQuery.cache[ someInternalNodeID ];
Theoretically, you may therefore also remove jQuery without leaks occurring from any references. jQuery even supports multiple separate instances of the library, each holding it's own set of data or events.
You can see John Resig explaining this stuff in the "The DOM Is a Mess" presentation.
The whole point of jQuery is to abstract away from crappy JavaScript implementations and bugs in browsers.. such as memory leaks :)
.. Yup; all associated data to an element will be removed when that element is removed from the DOM.
By and large, javascript is fairly good about knowing when it's appropriate to collect the garbage, and unless you're writing very large-scale or long-running client-side apps, I'd say the memory involved is mostly inconsequential and trying to second-guess it isn't gonna gain you a lot.
Determining what unfinished lexical closures or other tricky javascript in jQuery might still be accessing your given data could be pretty complicated in some cases.
As far as I'm aware, though, if you store a reference to whatever you got with jQuery's data function then it would continue to exist after the element is removed, so removing that reference would be necessary as well. Some simple test cases would give you a more definite answer.

Categories

Resources