GC thu hồi những gì không còn tham chiếu tới được từ gốc (window, stack, closure đang sống). Rò rỉ xảy ra khi code vô tình giữ tham chiếu tới thứ đáng lẽ phải bỏ — GC coi là còn dùng nên không thu.
Các nguồn hay gặp:
- Event listener không gỡ. Gắn listener lên
window/documenttrong component rồi khôngremoveEventListenerkhi unmount. Handler giữ closure, closure giữ cả cây component. - Detached DOM node. Gỡ node khỏi cây nhưng biến JS vẫn trỏ tới nó, kéo theo toàn bộ node con.
- Timer không clear.
setIntervalchạy mãi, giữ mọi thứ trong closure của nó. - Cache không giới hạn.
Map/object dùng làm cache, chỉ thêm chưa bao giờ xoá. - Biến toàn cục vô tình do quên
const(không xảy ra ở strict mode / module).
js
useEffect(() => {
const onScroll = () => { /* ... */ };
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll); // bắt buộc
}, []);Công cụ chẩn đoán: DevTools → Memory → chụp heap snapshot ở hai thời điểm, so sánh và lọc "Detached" để thấy node bị giữ; hoặc Performance → Memory xem đường heap có tụt xuống sau mỗi lần GC hay cứ leo dốc.
WeakMap/WeakSet giữ tham chiếu yếu — thích hợp khi cần gắn metadata cho object mà không muốn cản GC.