Layout thrashing
Tìm hiểu cách việc đọc thuộc tính hình học ngay sau khi ghi style buộc trình duyệt phải flush layout queue đồng bộ, tạo ra thảm họa O(N) reflow phá nát FPS.
Cơ chế Forced Synchronous Reflow (FSR) hoạt động ra sao?
Trình duyệt cực kỳ thông minh: khi bạn thay đổi các thuộc tính hình học của phần tử (như width, height, margin, top), nó không lập tức tính toán lại layout ngay lập tức. Thay vào đó, nó đưa các thay đổi này vào một 'Write Queue' (hàng đợi ghi) và lên lịch sẽ xử lý một lượt (batch) vào cuối frame để tối ưu hóa hiệu năng và tránh lãng phí tài nguyên CPU.
Tuy nhiên, nếu ngay sau khi ghi một style mới, bạn lại thực hiện đọc một thuộc tính hình học cần tính toán chính xác (chẳng hạn như offsetWidth, clientHeight, scrollTop, hoặc gọi getBoundingClientRect()), trình duyệt sẽ rơi vào thế tiến thoái lưỡng nan. Nó không thể trả về giá trị cũ vì style đã bị đánh dấu là thay đổi (dirty), và nó cũng không thể trì hoãn đến cuối frame. Để trả về số liệu chính xác cho JavaScript, trình duyệt buộc phải 'flush' hàng đợi ghi ngay lập tức và tính toán lại layout đồng bộ ngay tại thời điểm đó. Hiện tượng này được gọi là Forced Synchronous Reflow (FSR).
Khi hành vi đọc-ghi xen kẽ này xảy ra liên tục bên trong một vòng lặp (loop), trình duyệt sẽ phải tính toán đi tính toán lại layout sau mỗi vòng lặp đơn lẻ. Nếu bạn có 100 phần tử, thay vì 1 lần reflow cho tất cả ở cuối frame, bạn sẽ có 100 lần reflow liên tiếp. Đây chính là Layout Thrashing — kẻ thù số một gây drop frame rõ rệt và làm đơ giao diện người dùng.
// ❌ BAD: Đọc-Ghi xen kẽ gây Layout Thrashing khủng khiếp
els.forEach(el => {
const currentWidth = el.offsetWidth; // ĐỌC (Buộc browser phải tính toán lại layout)
el.style.width = (currentWidth + 10) + 'px'; // GHI (Làm bẩn layout hiện tại)
});
// GOOD: Gom toàn bộ READ trước, thực hiện WRITE sau
const currentWidths = els.map(el => el.offsetWidth); // Gom tất cả READ vào một lượt
els.forEach((el, index) => {
el.style.width = (currentWidths[index] + 10) + 'px'; // Chỉ WRITE, không xen kẽ đọc
});
// BETTER: Sử dụng requestAnimationFrame để trì hoãn việc WRITE xuống frame tiếp theo
els.forEach(el => {
const currentWidth = el.offsetWidth; // READ ở frame hiện tại (an toàn)
requestAnimationFrame(() => {
el.style.width = (currentWidth + 10) + 'px'; // WRITE ở frame render tiếp theo
});
});NÊNLuôn viết code theo mô hình: Đo lường trước (DOM Reads), thay đổi diện mạo sau (DOM Writes).
KHÔNGKhông bao giờ đọc các thuộc tính hình học (offsetWidth, getBoundingClientRect) bên trong các vòng lặp đang thay đổi style của DOM.
MẸOSử dụng tab Performance trong Chrome DevTools để phát hiện các cảnh báo màu đỏ dạng 'Forced Reflow' và lần theo stack trace để sửa.