I'm working on a graph visuzalization using D3 in a backbone view. I allow the user to pinch-zoom the graph, smoothly transitioning using webkit transforms, and redrawing on release. To keep the code simple, I'm just redrawing the graph at the new scale, rather than recaluclating the new positions and sizes for the elements (this was my original approach, but my team requested the redraw route).
[ I spoke with Bostock via twitter. This is actually not the preferred way of doing things ]
The thing I am noticing is that for each redraw, I'm dumping tons of dom nodes that aren't cleaned up.
This isn't related to circular references within event handlers/closures, as I've disabled all but my labels (these have no handlers attached), and the same behavior occurs.
I've tried aggressively removing the elements from the graph, but the dom nodes still seem to leak.
Here's some relevant code. 'render' is called for a new set of labels. Upon finishing zooming, 'close' is called on the old graph, and a new one is created with another view instantiation and call to 'render' :
render: function() {
// create the svg offscreen/off dom
//document.createElementNS(d3.ns.prefix.svg, "svg")
var svg = this.svg = d3.select(this.el)
.append("svg:svg")
.attr('width', this.VIEW_WIDTH)
.attr('height', this.VIEW_HEIGHT)
this._drawTimeTicks.call(this, true);
return this;
},
_drawTimeTicks: function(includeLabels) {
var bounds = this.getDayBounds();
var min = bounds.start;
var date = new Date(min);
var hour = 1000 * 60 * 60;
var hourDiff = 60 * this.SCALE;
var graphX = (date.getTime() - min) / 1000 / 60;
var textMargin = 7;
var textVert = 11;
// Using for loop to draw multiple vertical lines
// and time labels.
var timeTicks = d3.select(this.el).select('svg');
var width = timeTicks.attr('width');
var height = timeTicks.attr('height');
for (graphX; graphX < width; graphX += hourDiff) {
timeTicks.append("svg:line")
.attr("x1", graphX)
.attr("y1", 0)
.attr("x2", graphX)
.attr("y2", height)
.classed('timeTick');
if (includeLabels) {
timeTicks.append("svg:text")
.classed("timeLabel", true)
.text(this.formatDate(date))
.attr("x", graphX + textMargin)
.attr("y", textVert);
}
date.setTime(date.getTime() + hour);
}
close: function() {
console.log("### closing the header");
this.svg.selectAll('*').remove();
this.svg.remove();
this.svg = null;
this.el.innerHTML = '';
this.unbind();
this.remove();
}
As you can see, I'm not doing anything tricky with event handlers or closures. With a few zoom interactions I can leak dozens of dom nodes that are never reclaimed by GC.
Is this a memory leak, or is d3 doing something behind the scenes to optimize future graph construction/updates? Is there some better way to destroy a graph that I'm not aware of?
Any ideas?
D3 doesn't keep any hidden references to your nodes, so there's no internal concept of "DOM node cleanup". There are simply selections, which are arrays of DOM elements, and the DOM itself. If you remove an element from the DOM, and you don't keep any additional references to it, it should be reclaimed by the garbage collector.
(Aside: it's not clear whether the leak you are referring to is elements remaining in the DOM or orphaned elements not being reclaimed by the garbage collector. In the past, some older browsers had bugs garbage-collecting circular references between DOM elements and JavaScript closures, but I'm not aware of any such issues that affect modern browsers.)
If you are updating the DOM, the most performant way of doing this is (generally) using a data-join, because this allows you to reuse existing elements and only modify the attributes that need to change. Using a key function for the data-join is also a good idea for object constancy; if the same data is displayed both before and after the update, then you may not need to recompute all of its attributes, and the browser does less work to update.
In certain cases, there are alternatives to arbitrary updates that are faster, such as updating the transform attribute of an enclosing G element rather than updating the positions of descendant elements. As another example, you can do geometric zooming within an SVG element simply by changing the viewBox attribute. But geometric zooming is a very limited case; in general, the most efficient update depends on what precisely is changing. Use a data-join so that you can minimize the number of elements you append or remove, and minimize the number of attributes you need to recompute.
A couple other things I'll point out…
You could use a data-join to create multiple ticks at the same time, rather than using a for loop. For loops are almost never used with D3 since the data-join can create multiple elements (and hierarchical structures) without loops. Even better, use the axis component (d3.svg.axis) and a time scale (d3.time.scale) with a time format (d3.time.format).
Recent versions of D3 don't require the "svg:" namespace, so you can append "text", "line", etc.
I can't think of any situation where selectAll("*").remove()
makes sense. The "*" selector matches all descendants, so this would remove every single descendant from its parent. You should always try to remove the top-most element—the SVG container, here—rather than redundant removals of child elements.
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