Reputation: 30330
It appears that event-handling closures can cause DOM nodes to be leaked if they refer to a d3.js selection that uses a keyed data join.
Why does this happen? Is it an issue with d3.js or with the way it is being invoked?
This example leaks HTMLLIElement
objects when step
is called repeatedly (clickHandler
does not have to be executed):
function getKeys(n) {
// returns a random array of n unique Strings, e.g. ["Alpha", "Quebec", "Charlie"]
}
function step() {
function clickHandler() {
// removing this reference removes the leak
// (note that the outer variable is pulled into closure scope regardless of whether this function is called).
listItems;
}
var keys = getKeys(3);
var listItems = d3.selectAll('li')
.data(keys, function(d) { return d });
listItems.enter()
.append('li')
.text(function(d) { return '#' + d })
.on('click', clickHandler)
listItems.exit()
.remove()
}
This pattern is reproducible with D3.js 3.5.3 and identifiable in Chrome 39.
It appears that DOM nodes are leaked when two criteria are met:
Any of these steps prevent a memory leak:
data
listItems = null
at the end of step
listItems = null
in the click handling closure.The latter point is especially interesting because it releases all of the leaked nodes, not just those in the current listItems
selection. This implies that the selections are linked, which I did not expect.
Inspecting a heap snapshot in Chrome DevTools shows that the leaked HTMLLIElement
objects have two distinct listItems
in their retainer hierarchy:
Is this expected behaviour? If so, what causes it? Is this a memory leak in my code or in d3.js?
Upvotes: 3
Views: 943
Reputation: 89
Pretty interesting. I caught a memory leak caused by d3.transition(). Basically what happened was chart hanged after being inactive for a while - first the updates stopped and then the whole tab crashed with chart completely disappearing, while task manager showed huge memory usage for it, growing slowly. Removing the transitions fixed all issues mentioned.
Upvotes: 0
Reputation: 86
In the enter phase where new elements are being added, you are binding to each newly added 'li' element's onClick handler.
listItems.enter()
.append('li')
.text(function(d) { return '#' + d })
.on('click', clickHandler);
In the exit phase you are removing the 'li' elements that are no longer needed. However, you are not unbinding from the onClick handler before removing the 'li' element.
In the profiler image you posted, notice that the HTMLLIElement is colored in red. Chrome's memory profiler is telling you that HTMLIElement is disconnected from the DOM tree but there is still a javascript reference to it. In this case the onClick handler for the 'li' element has a reference to your js code.
Remove the click handler by calling .on('click',null)
in the exit phase of D3.
listItems.exit()
.on('click', null)
.remove();
Will get rid of the reference to your clickHandler.
Upvotes: 3