Memory leak là khi bộ nhớ đáng lẽ được giải phóng nhưng vẫn bị giữ reference từ root (window/global), nên garbage collector (mark-and-sweep) coi là còn dùng và không dọn. Heap phình dần theo thời gian, app chậm rồi crash.
Các nguồn leak hay gặp:
- Timer không clear: setInterval/setTimeout không gọi clearInterval khi không cần. Callback giữ nguyên closure và mọi biến nó tham chiếu.
- Event listener không gỡ: addEventListener trên window/document mà quên removeEventListener.
- Detached DOM: node đã xoá khỏi cây DOM nhưng còn biến JS trỏ tới, nên cả subtree không được GC.
- Cache/collection vô hạn: push vào array hay Map global mà không bao giờ xoá. Dùng WeakMap/WeakRef để key không bị giữ sống.
- Biến global vô tình: quên let/const hoặc gán window.x khiến biến gắn vào global mãi.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // cleanup khi unmount
}, []);Phát hiện: Chrome DevTools → Memory → Heap snapshot, chụp 2 lần rồi so sánh xem object nào tăng; hoặc Performance panel bật Memory xem đường heap leo dốc đều thay vì răng cưa. Node: process.memoryUsage() log định kỳ và heap dump qua --inspect.
Phòng tránh: cleanup trong useEffect return, AbortController cho fetch/listener, WeakMap/WeakRef cho cache, null hoá reference khi xong việc.
A memory leak is when memory that should be freed stays referenced from a root (window/global), so the garbage collector (mark-and-sweep) still treats it as reachable and never reclaims it. The heap grows over time until the app slows down or crashes.
Common sources:
- Uncleared timers: setInterval/setTimeout without clearInterval. The callback keeps its whole closure alive.
- Unremoved listeners: addEventListener on window/document without removeEventListener.
- Detached DOM: a node removed from the DOM tree but still referenced by a JS variable, keeping the whole subtree alive.
- Unbounded caches: pushing into a global array or Map that is never cleared. Use WeakMap/WeakRef so keys do not stay alive.
- Accidental globals: forgetting let/const or assigning window.x.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // cleanup on unmount
}, []);Detection: Chrome DevTools → Memory → Heap snapshot; take two and diff to find growing objects. Or the Performance panel with Memory enabled — a steadily climbing heap (vs a sawtooth) signals a leak. Node: log process.memoryUsage() periodically and capture heap dumps via --inspect.
Prevention: clean up in useEffect return, use AbortController for fetches/listeners, WeakMap/WeakRef for caches, and null out references when done.