disconnectedCallback and Cleanup
When disconnectedCallback fires
Section titled “When disconnectedCallback fires”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
setIntervalorsetTimeouttimers - Close WebSocket connections
- Disconnect
MutationObserver,ResizeObserver, orIntersectionObserverinstances
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.
The symmetry pattern
Section titled “The symmetry pattern”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.
Full timer pattern
Section titled “Full timer pattern”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);Live demo
Section titled “Live demo”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.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Clean up everything in disconnectedCallback | Prevents memory leaks, CPU leaks, and dangling event listeners completely — a clean symmetry with connectedCallback | Always requires paired setup/teardown logic — more boilerplate and a risk of forgetting the matching half |
| Let the garbage collector handle it | Shorter code, no disconnectedCallback to write | Listeners, 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) |
Common mistakes
Section titled “Common mistakes”- Not implementing
disconnectedCallbackat all — event listeners,setInterval/setTimeout, and observers set up inconnectedCallbackkeep 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
disconnectedCallbackfires 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 thebeforeunloadorpagehideevent instead of relying ondisconnectedCallback.
💡 Real-world example
GitHub — the
<relative-time>element usessetIntervalto keep updating text like “3 minutes ago”, and must clear that interval indisconnectedCallbackevery time so it doesn’t leak when a timeline is scrolled and elements are created/destroyed in bulk. (github/relative-time-element)