Họ đang đo gì
Bạn có quy trình chẩn đoán không, và có biết dùng heap snapshot không. Đây là câu rất khó diễn nếu chưa từng làm.
Trả lời ngắn~30 giây
Mình đo trước: chụp heap snapshot, thao tác lặp lại một luồng vài chục lần, ép GC, chụp lần hai rồi so sánh phần “Objects allocated between snapshots”. Thứ còn sống mà lẽ ra phải chết chính là manh mối, và cây retainer chỉ thẳng ra ai đang giữ nó. Bốn thủ phạm quen mặt: listener không gỡ, timer không clear, tham chiếu tới node DOM đã bị gỡ, và cache tự viết không có giới hạn.
Giải thích sâu
Chi tiết làm phép đo này đáng tin là ba lần snapshot chứ không phải hai. Chụp lần một, chạy luồng, chụp lần hai, chạy luồng lần nữa, chụp lần ba — rồi so lần ba với lần hai. Cách này loại được những thứ chỉ khởi tạo một lần và làm nhiễu phép so hai lần. Nếu đường bộ nhớ đi lên theo bậc thang và không bao giờ về, đó là rò rỉ; nếu nó răng cưa quanh một mức thì đó là hành vi bình thường của GC.
Detached DOM là loại khó thấy nhất. Bạn gỡ một node khỏi cây, nhưng một mảng trong closure còn giữ tham chiếu tới nó, nên cả cây con đó — có thể là hàng nghìn node — vẫn sống trong heap mà không hiển thị ở đâu. DevTools có bộ lọc “Detached” trong heap snapshot làm đúng việc này. Trong React, nguyên nhân thường là một ref được lưu vào state toàn cục hoặc một sự kiện đăng ký ở window mà useEffect không trả về hàm dọn dẹp.
Về cache tự viết: một Map dùng làm bộ nhớ đệm mà không có giới hạn kích thước hay TTL không phải cache, nó là rò rỉ có chủ đích. WeakMap giúp khi khoá là object và bạn muốn nó biến mất cùng khoá, nhưng WeakMap không giải quyết được cache khoá bằng chuỗi. Với trường hợp đó cần LRU có giới hạn thật.
Một điều đáng nói ở mức senior: không phải cứ bộ nhớ tăng là rò rỉ. GC của V8 chỉ dọn khi thấy cần, nên đường đồ thị đi lên trong vài phút là bình thường. Kết luận rò rỉ chỉ nên đưa ra sau khi ép GC mà mức nền vẫn cao hơn lần trước — và nói được điều đó cho thấy bạn phân biệt được triệu chứng với nguyên nhân.
Bốn nguồn rò rỉ, và bản sửa
useEffect(() => {
const onResize = () => layout(rows);
window.addEventListener('resize', onResize);
const id = setInterval(poll, 5000);
const ac = new AbortController();
fetch(url, { signal: ac.signal });
return () => { // <- thiếu dòng nào cũng là rò rỉ
window.removeEventListener('resize', onResize);
clearInterval(id);
ac.abort();
};
}, [rows]);
// Cache có giới hạn, không phải Map vô hạn
const cache = new Map<string, Result>();
if (cache.size > 500) cache.delete(cache.keys().next().value);Câu hỏi tiếp theo họ sẽ hỏi
?WeakMap giúp được gì?
Khoá được giữ yếu: khi không còn ai tham chiếu tới object dùng làm khoá, cả mục đó biến mất mà bạn không phải xoá tay. Rất hợp để gắn dữ liệu phụ vào một object bạn không sở hữu. Nó không duyệt được và không có size, đó là cái giá.
?Đo trên production thế nào?
performance.memory không đáng tin và bị giới hạn; API tốt hơn là measureUserAgentSpecificMemory(), cần cross-origin isolation. Thực tế thì nhiều đội đo gián tiếp: tỷ lệ tab bị trình duyệt kill, và thời gian phiên trước khi hiệu năng tụt.
Trả lời thế này là mất điểm
- Đề xuất “gán
nullđể giải phóng bộ nhớ” như giải pháp chính. GC dựa vào khả năng tiếp cận, gán null chỉ có ích khi nó cắt đúng tham chiếu cuối cùng. - Đoán nguyên nhân mà không chụp snapshot. Rò rỉ bộ nhớ là bài toán đo, không phải bài toán suy luận.