Skip to content

disconnectedCallback and Cleanup

disconnectedCallback is called each time the element is removed from a connected document. Common cleanup tasks belong here:

  • Remove event listeners added in connectedCallback
  • Cancel setInterval or setTimeout timers
  • Close WebSocket connections
  • Disconnect MutationObserver, ResizeObserver, or IntersectionObserver instances

One important caveat: disconnectedCallback is not called when the entire page is unloaded (e.g. the user navigates away or closes the tab). It is only triggered by explicit DOM removal such as removeChild, replaceWith, or innerHTML reassignment.

Think of connectedCallback and disconnectedCallback as matching brackets. Whatever resource you acquire when the element enters the DOM, you release when it leaves:

class TimerDemo extends HTMLElement {
connectedCallback() {
// start work
this._intervalId = setInterval(this._tick.bind(this), 1000);
}
disconnectedCallback() {
// stop work — mirror of connectedCallback
clearInterval(this._intervalId);
}
_tick() {
// called every second while connected
}
}

Storing the handle (this._intervalId) on the element instance is the standard way to make it accessible to disconnectedCallback.

class TimerDemo extends HTMLElement {
connectedCallback() {
this._seconds = 0;
this._render();
this._intervalId = setInterval(this._tick.bind(this), 1000);
}
disconnectedCallback() {
clearInterval(this._intervalId);
}
_tick() {
this._seconds += 1;
this._render();
}
_render() {
this.textContent = 'Running: ' + this._seconds + 's';
}
}
customElements.define('timer-demo', TimerDemo);

Click Remove timer to remove the element from the DOM — the log line confirms disconnectedCallback fired and the timer stopped. Click Re-add timer to re-insert a fresh element; the counter resets and the log shows connectedCallback running again.

OptionBenefitCost
Clean up everything in disconnectedCallbackPrevents memory leaks, CPU leaks, and dangling event listeners completely — a clean symmetry with connectedCallbackAlways requires paired setup/teardown logic — more boilerplate and a risk of forgetting the matching half
Let the garbage collector handle itShorter code, no disconnectedCallback to writeListeners, timers, and observers that still reference the element aren’t collected until every reference is gone — causing memory and CPU leaks, especially for elements created/destroyed repeatedly (e.g. in a scrolling list)
  • Not implementing disconnectedCallback at all — event listeners, setInterval/setTimeout, and observers set up in connectedCallback keep running even after the element is removed from the DOM, accumulating memory leaks as elements are created and destroyed repeatedly.
  • Forgetting to disconnect a MutationObserver/ResizeObserver/IntersectionObserver — these observers hold a reference to the element and its callback, so the garbage collector can’t reclaim them until you call .disconnect() explicitly.
  • Assuming disconnectedCallback fires on page unload — it isn’t reliably called when the user closes the tab or refreshes. If you need to save data before the page goes away, use the beforeunload or pagehide event instead of relying on disconnectedCallback.

💡 Real-world example

GitHub — the <relative-time> element uses setInterval to keep updating text like “3 minutes ago”, and must clear that interval in disconnectedCallback every time so it doesn’t leak when a timeline is scrolled and elements are created/destroyed in bulk. (github/relative-time-element)

When does disconnectedCallback fire?
What happens to a setInterval started in connectedCallback if you never call clearInterval in disconnectedCallback?
Does disconnectedCallback fire when the user navigates away from the page?