ข้ามไปยังเนื้อหา

disconnectedCallback และการ Cleanup

disconnectedCallback จะถูกเรียกทุกครั้งที่ element ถูกลบออกจาก document ที่เชื่อมต่ออยู่ งานทำความสะอาดที่ควรทำที่นี่ได้แก่:

  • ลบ event listener ที่เพิ่มไว้ใน connectedCallback
  • ยกเลิก timer ของ setInterval หรือ setTimeout
  • ปิด WebSocket connections
  • Disconnect MutationObserver, ResizeObserver หรือ IntersectionObserver

ข้อควรระวังสำคัญ: disconnectedCallback จะไม่ถูกเรียกเมื่อ page ทั้งหมดถูก unload (เช่น ผู้ใช้เปิดหน้าใหม่หรือปิด tab) จะถูก trigger เฉพาะจากการลบ DOM อย่างชัดเจน เช่น removeChild, replaceWith หรือการกำหนดค่าใหม่ให้ innerHTML

คิดว่า connectedCallback และ disconnectedCallback เป็นเหมือนวงเล็บที่จับคู่กัน ทรัพยากรที่คุณได้มาเมื่อ element เข้าสู่ DOM คุณก็ต้องคืนตอน element ออกไป:

class TimerDemo extends HTMLElement {
connectedCallback() {
// เริ่มทำงาน
this._intervalId = setInterval(this._tick.bind(this), 1000);
}
disconnectedCallback() {
// หยุดทำงาน — สมมาตรกับ connectedCallback
clearInterval(this._intervalId);
}
_tick() {
// ถูกเรียกทุก 1 วินาทีขณะที่ connected
}
}

การเก็บ handle (this._intervalId) ไว้บน element instance คือวิธีมาตรฐานเพื่อให้ 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);

คลิก Remove timer เพื่อลบ element ออกจาก DOM — บรรทัด log ยืนยันว่า disconnectedCallback ทำงานและ timer หยุดแล้ว คลิก Re-add timer เพื่อแทรก element ใหม่อีกครั้ง counter จะ reset และ log จะแสดง connectedCallback ทำงานอีกครั้ง

ตัวเลือกBenefitCost
Cleanup ทุกอย่างใน disconnectedCallbackป้องกัน memory leak, CPU leak และ event listener ค้างอย่างครบถ้วน symmetry ชัดเจนกับ connectedCallbackต้องเขียน logic คู่กันเสมอ (setup/teardown) เพิ่ม boilerplate และเสี่ยงลืมคู่ที่ตรงกัน
ปล่อยให้ garbage collector จัดการเองโค้ดสั้นลง ไม่ต้องเขียน disconnectedCallbacklistener, timer, observer ที่ยังอ้างอิงถึง element จะไม่ถูกเก็บกวาดจนกว่า reference ทั้งหมดจะหลุด ทำให้เกิด memory และ CPU leak โดยเฉพาะกับ element ที่ถูกสร้าง/ลบซ้ำๆ (เช่น ใน list ที่ scroll)
  • ไม่ implement disconnectedCallback เลย — event listener, setInterval/setTimeout, และ observer ที่ตั้งไว้ใน connectedCallback จะยังทำงานต่อแม้ element ถูกลบออกจาก DOM แล้ว ทำให้เกิด memory leak สะสมเมื่อ element ถูกสร้างและลบซ้ำๆ
  • ลืม disconnect MutationObserver/ResizeObserver/IntersectionObserver — observer เหล่านี้ถือ reference ไปยัง element และ callback ทำให้ garbage collector เก็บกวาดไม่ได้จนกว่าจะเรียก .disconnect() อย่างชัดเจน
  • สมมติว่า disconnectedCallback จะถูกเรียกตอน page unload — ไม่ถูกเรียกอย่างน่าเชื่อถือเมื่อผู้ใช้ปิด tab หรือ refresh หน้า ถ้าต้องเซฟข้อมูลก่อนออกจากหน้า ให้ใช้ event beforeunload หรือ pagehide แทน ไม่ใช่พึ่งพา disconnectedCallback

💡 ตัวอย่างจากของจริง

GitHub<relative-time> element ของ GitHub ใช้ setInterval เพื่ออัปเดตข้อความ “3 minutes ago” และต้อง clear interval ใน disconnectedCallback ทุกครั้งเพื่อไม่ให้ leak เมื่อ timeline ถูก scroll และ element ถูกสร้าง/ลบซ้ำจำนวนมาก

X (Twitter) — timeline แบบ virtualized ของ X สร้างและทำลาย custom element จำนวนมากขณะ scroll การไม่ cleanup listener ใน disconnectedCallback อย่างถูกต้องจะทำให้ memory ค่อยๆ โตขึ้นเรื่อยๆ จนหน้าเว็บหน่วง

disconnectedCallback ทำงานเมื่อไหร่?
จะเกิดอะไรขึ้นกับ setInterval ที่เริ่มใน connectedCallback ถ้า disconnectedCallback ไม่ได้เรียก clearInterval?
disconnectedCallback ทำงานเมื่อผู้ใช้เปิดหน้าใหม่หรือ unload page หรือไม่?