JS tự quản bộ nhớ: vòng đời gồm cấp phát → dùng → giải phóng. Việc giải phóng do garbage collector (GC) lo tự động.
GC hiện đại dùng thuật toán mark-and-sweep: bắt đầu từ tập root (global object, call stack hiện tại), đánh dấu mọi object còn với tới được (reachable) qua chuỗi tham chiếu, rồi quét bỏ phần không đánh dấu. Nói cách khác: object được giữ chừng nào còn tham chiếu tới được từ root; không ai với tới thì bị thu hồi. (Thuật toán đếm tham chiếu cũ có nhược điểm với vòng tham chiếu nên không còn dùng.)
Memory leak = object đáng lẽ bỏ nhưng vẫn vô tình bị giữ reachable nên GC không thu. Các nguồn thường gặp:
- Biến global ngoài ý muốn (quên let/const, gán vào window) — sống suốt đời trang.
- Timer/interval không hủy: setInterval giữ closure và biến nó tham chiếu mãi.
- Event listener không gỡ: listener trên DOM/window giữ luôn phần tử và scope liên quan.
- Closure giữ tham chiếu lớn không cần thiết.
- Tham chiếu tới DOM đã gỡ (detached node) còn lưu trong biến/mảng.
Giảm rò rỉ: dọn trong cleanup (clearInterval, removeEventListener, AbortController) — trong React là hàm cleanup của useEffect; dùng WeakMap/WeakSet cho cache gắn theo object (tham chiếu yếu, không chặn GC); và dùng heap snapshot của DevTools để truy nguồn.