I'm using jquery-ui Tabs and I'm having a problem that occurs when a tab has been removed. The tab appears to be removed, along with its content div but when you take a look at the heap in Chrome DevTools Profiles (after a tab has been removed) you'll see that the tab li and div elements are still present, but detached. Over time, the repeated addition/removal of tabs causes these elements to accumulate. For example, if you add a tab 10 times, there will be 10 detached div elements and 10 detached li elements showing up in the heap snapshot:
I have the following views:
TabLabel = Marionette.ItemView.extend({
template: "#tab-label",
tagName: "li",
events: {
"click .ui-icon-close": "closeTab"
},
closeTab: function(e) {
this.$el.contents().remove();
this.model.collection.remove(this.model);
$("#main-container").tabs("refresh");
this.close();
}
});
TabContainer = Marionette.ItemView.extend({
template: "#tab-container",
tagName: "div",
onBeforeRender: function() {
this.$el.attr("id", "div-" + this.id);
},
onClose: function() {
// This removes the region that contains the container
App.layout.removeRegion(this.containerRegion);
}
});
TabLabels = Marionette.CollectionView.extend({
tagName: "ul"
});
TabContainers = Marionette.CollectionView.extend({
tagName: "div"
});
The views are instantiated like so:
tabs = new TabsCollection(); // Create a new collection instance
var tabLabelView = new TabLabels({
itemView: TabLabel,
collection: tabs
});
var tabContainerView = new TabContainers({
itemView: TabContainer,
collection: tabs
});
As you can see, the views both refer to the same collection; each model in the collection can be said to represent a single tab (it just so happens that the model contains the necessary information to satisfy jquery-ui tabs). The views are shown in a region via Marionette.Layout... pretty standard. Adding a tab is accomplished by clicking a link in the tab container; all this does is adds another model to the collection and then calls tabs("refresh") on the main container, which makes the new tab appear. Removing a tab is accomplished by clicking an "X" icon in the upper right-hand corner of the tab.
I've spent a lot of time trying to track down this leak and I can't figure out if it's a problem in my code (the way I'm closing views/models/etc. perhaps?) or if it's a problem in the jquery-ui tabs plugin.
Thoughts? Thanks in advance!
Update #1 As requested, here is a jsfiddle demonstrating the problem -- just close the tabs and you'll see that detached elements are left behind.
Also, a screenshot:
Update #2 This appears to be a leak in the jquery-ui tabs widget. The same behavior occurs in the widget demonstration on the jquery-ui website. I added a few tabs and then closed them out and sure enough, they persisted:
I've tested this with the latest (at the time of this writing) version of jQuery UI (1.10.3) and the previous version (1.10.2).
Detached DOM elements are the elements which have been removed from the DOM but their memory is still retained because of JavaScript. This means that as long the element have a reference to any variable or an object anywhere, it does not garbage collected even after destroyed from the DOM.
A node is said to be "detached" when it's removed from the DOM tree but some JavaScript still references it. Detached DOM nodes are a common cause of memory leaks. This section teaches you how to use DevTools' heap profilers to identify detached nodes.
The easiest way to access a single element in the DOM is by its unique ID. You can get an element by ID with the getElementById() method of the document object. In the Console, get the element and assign it to the demoId variable. Logging demoId to the console will return our entire HTML element.
Is there a reason why you use this.$el.contents().remove()
instead of this.$el.empty()
?
Using this.$el.empty()
in that jsFiddle of yours seemed to remedy the detached NodeList.
A few notes memory profiling:
I have found cases where jQuery seems to leek because they save the current jQuery object in .prevObject
when doing some operations like calling .add()
. Maybe that call to .contents()
do some funky magic.
So I eventually fixed this problem (quite some time ago) by doing two things:
Changing the closeTab function (see the original jsFiddle) to the following:
closeTab: function (e) {
e.stopPropagation();
e.preventDefault();
this.model.collection.remove(this.model);
this.close();
}
In that snippet, stopPropagation and preventDefault are really what stopped this element from remaining detached (and accumulating on subsequent adds/removes) in the DOM. To summarize: this problem was created by not calling stopPropagation/preventDefault on the event object after it was triggered. I've updated the jsFiddle so that it correctly removes the tab elements and for posterity, here's a screenshot of the result:
As you can see, none of the tab elements are remaining detached after clicking the "X" to close them. The only detached elements are the ones that come from jsFiddle's interface.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With