d3.transition timer cleanup when removing elements during animation
0 reputation · 09 Feb 2022, 12:42 UTC
0 reputation · 09 Feb 2022, 12:42 UTC
d3.transition schedules animation frames through d3.timer, which retains a closure reference to the selection. When a target element is removed from the DOM before the transition ends, the timer callback continues to hold that reference, preventing garbage collection of the detached node and its associated transition context.
The API provides transition.interrupt() to cancel active transitions and release timer references, and a 'cancel' event (v6+) for custom cleanup logic. However, neither is automatically invoked when the underlying element is removed externally. Developers must manually call interrupt() before removal or attach an 'end' listener that also handles premature detachment.
It is unclear whether the 'cancel' event fires when a transition is interrupted due to element removal versus an explicit interrupt() call, and whether timer.stop() on the internal timer is required in addition to interrupt() for complete reference release. The documentation does not specify if interrupt() implicitly stops the associated timer or if a separate timer reference must be tracked.
What is the exact sequence of events when an element with a running transition is removed from the DOM without prior interrupt()? Does the 'cancel' event fire in that scenario, and is timer.stop() necessary after interrupt() to fully release the closure?
34775 reputation · 09 Feb 2022, 22:50 UTC
If you simply remove() a node that has an active d3.transition, the timer that drives the transition keeps firing on every animation frame. The callback still receives the transition object, but because the node is no longer in the document the transition logic immediately exits. The timer itself is not cancelled, so the closure that holds a reference to the node stays alive until the page unloads or another transition clears it. This can lead to a memory leak if many nodes are removed that way.
cancel event fire?Only when transition.interrupt() is called does D3 dispatch a cancel event. Detaching the node from the DOM by any other means (e.g., node.parentNode.removeChild(node), selection.remove(), or a CSS display:none) does not trigger the cancel event. Consequently, you cannot rely on that event for cleanup in the removal‑only scenario.
timer.stop() required?No. transition.interrupt() internally stops the associated d3.timer instance and clears the callback. After interrupt() returns, the timer is removed from the active list and the closure reference is released. Calling timer.stop() again would be redundant and has no effect.
selection.interrupt() on the selection that owns the transition.
selection.transition()
.duration(1000)
.attr("opacity", 0);
// Later, before removal
selection.interrupt(); // cancels the transition and stops the timer
selection.remove(); // now safe to detach
cancel listener to perform any extra logic.
selection.transition()
.duration(1000)
.attr("opacity", 0)
.on("cancel", () => {
// cleanup, logging, etc.
});
interrupt() will be called (e.g., the element may be removed by external code), consider using a MutationObserver to detect removal and trigger interrupt() automatically.
To verify that a transition has been cancelled and its timer stopped, open Chrome DevTools, run the code above, then take a heap snapshot before and after removal to confirm that the detached node is retained without interrupt() and freed with it.
What version of D3 are you using? The behavior described applies to v6+; earlier releases may differ slightly in event handling.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.