Layout Thrashing
Hiện tượng xảy ra khi JavaScript đọc và ghi thuộc tính layout liên tục, gây tái tính toán bố cục nhiều lần trong một khung hình.
Layout Thrashing là gì?
Layout Thrashing (rối loạn bố cục) là hiện tượng trình duyệt phải tính lại vị trí và kích thước các phần tử trên trang nhiều lần trong cùng một khung hình (frame), do JavaScript vừa đọc vừa ghi các thuộc tính liên quan đến layout — như offsetTop, clientWidth, getBoundingClientRect(), rồi sau đó thay đổi style.width, className hoặc thêm/xóa lớp CSS ảnh hưởng đến dòng chảy.
Kết quả: trình duyệt không thể tối ưu hóa, buộc phải chạy chuỗi đầy đủ Recalculate Styles → Layout → Paint → Composite lặp đi lặp lại — làm chậm hiển thị, tăng thời gian tương tác đầu tiên (FCP, FID, INP) và gây giật hình trên thiết bị yếu.
Tại sao quan trọng trong SEO?
Google xếp hạng dựa trên trải nghiệm người dùng thực tế — đặc biệt qua các chỉ số Core Web Vitals. Layout Thrashing trực tiếp làm xấu đi:
- INP (Interaction to Next Paint): tăng độ trễ phản hồi khi người dùng cuộn, nhấn nút hoặc nhập liệu;
- FID (First Input Delay): giảm khả năng xử lý sự kiện kịp thời;
- LCP (Largest Contentful Paint): nếu thrashing xảy ra trong giai đoạn tải ban đầu, có thể trì hoãn render nội dung chính;
- Tỷ lệ thoát và thời gian ở lại: trang giật, chậm khiến người dùng rời đi sớm — tín hiệu tiêu cực với thuật toán.
Theo báo cáo của Google Chrome UX Report (CrUX), trang có INP > 200ms có tỷ lệ thoát cao hơn trung bình 37% — và Layout Thrashing là một trong những nguyên nhân phổ biến nhất gây INP cao trên trang động.
Cách hoạt động
Trình duyệt xử lý layout theo cơ chế batching (gộp): nó đợi đến cuối khung hình (thường ~16ms cho 60fps), sau đó mới thực hiện một lần duy nhất Recalculate Styles + Layout. Nhưng khi JavaScript vừa đọc vừa ghi thuộc tính layout, trình duyệt buộc phải đẩy layout ra ngay lập tức để đảm bảo giá trị đọc là chính xác — phá vỡ cơ chế gộp.
Ví dụ đơn giản:
- JS đọc
el.offsetHeight→ trình duyệt phải layout ngay để trả kết quả đúng; - JS thay đổi
el.style.width = '200px'→ trình duyệt lại cần layout lại để cập nhật; - Nếu lặp lại 5 lần trong một vòng lặp → 5 lần layout riêng lẻ → thrashing.
Hướng dẫn thực hiện
Để tránh Layout Thrashing, áp dụng theo thứ tự ưu tiên:
- Đọc hết thuộc tính layout trước: thu thập toàn bộ giá trị cần thiết (
offsetHeight,getBoundingClientRect(),scrollHeight...) trong một lần duy nhất, trước bất kỳ thay đổi nào. - Ghi toàn bộ thay đổi sau cùng: nhóm tất cả thao tác DOM (thay đổi style, class, nội dung) vào một khối — tốt nhất là dùng
requestAnimationFrame()hoặcdocumentFragment. - Sử dụng API hiện đại hơn: thay vì
offsetTop, ưu tiênelement.getBoundingClientRect()(đã được cache trong frame), hoặc dùngResizeObserverđể theo dõi thay đổi kích thước mà không cần polling. - Tránh vòng lặp đọc-ghi xen kẽ: kiểm tra mã bằng công cụ DevTools → tab Performance → ghi lại hành vi, tìm dấu hiệu Forced reflow (màu vàng cam).
- Thử nghiệm với
will-change: transform: nếu phần tử thường xuyên thay đổi vị trí, khai báo trước giúp trình duyệt nâng cấp layer — giảm tần suất layout bắt buộc (tùy trường hợp).
Lỗi thường gặp
| Lỗi | Dấu hiệu trong DevTools | Cách khắc phục |
|---|---|---|
Vòng lặp for đọc offsetHeight rồi đổi style.margin |
Nhiều mục Layout liên tiếp trong timeline | Tách thành 2 vòng: 1 đọc hết, 1 ghi hết; hoặc dùng getComputedStyle(el).height nếu chỉ cần giá trị đã tính |
Dùng scrollTop hoặc scrollHeight trong hàm scroll event |
Layout xuất hiện mỗi khi cuộn 1px | Thêm debounce hoặc dùng IntersectionObserver thay thế |
Thay đổi class CSS ảnh hưởng đến layout trong requestAnimationFrame nhưng vẫn đọc layout bên trong |
Forced reflow ngay trong RAF callback | Chuyển việc đọc ra ngoài RAF; chỉ để việc ghi vào trong RAF |
Ví dụ thực tế
Một trang tin tức có thanh điều hướng cố định (sticky header) tự ẩn khi cuộn xuống. Đoạn mã ban đầu:
window.addEventListener('scroll', () => {
const scrollPos = window.scrollY;
const headerHeight = header.offsetHeight; // ĐỌC → layout bắt buộc
if (scrollPos > headerHeight) {
header.classList.add('hidden'); // GHI → layout lại
}
});
Sửa thành:
const headerHeight = header.offsetHeight; // ĐỌC MỘT LẦN, NGOÀI EVENT
window.addEventListener('scroll', () => {
if (window.scrollY > headerHeight) {
header.classList.add('hidden'); // CHỈ GHI
}
});
Kết quả: giảm 90% forced layout trong sự kiện cuộn — INP từ 320ms xuống còn 45ms trên thiết bị Android低端.
Câu hỏi thường gặp
Layout Thrashing có xảy ra trên mọi trình duyệt?
Có, nhưng mức độ ảnh hưởng khác nhau. Chrome và Edge (dựa trên Chromium) báo rõ Forced reflow trong Performance tab. Firefox cũng có cơ chế tương tự nhưng ít cảnh báo trực quan hơn. Safari xử lý tốt hơn với một số trường hợp, nhưng vẫn bị ảnh hưởng nếu đọc-ghi xen kẽ.
Có công cụ nào tự động phát hiện Layout Thrashing không?
Không có công cụ hoàn toàn tự động, nhưng bạn có thể dùng: (1) Chrome DevTools → Performance → record + chọn Enable advanced paint instrumentation; (2) Lighthouse phiên bản 10.0+ (trong phần Diagnostics) sẽ cảnh báo Uses inefficient CSS selectors hoặc Contains elements with non-composited animations — đây là dấu hiệu gián tiếp; (3) Thư viện layout-thrashing (Google) giúp phát hiện trong môi trường test.
Layout Thrashing ảnh hưởng đến SEO trên mobile nhiều hơn desktop?
Đúng. Thiết bị di động có CPU yếu hơn, RAM hạn chế và màn hình nhỏ hơn nên mỗi lần layout tốn nhiều tài nguyên hơn. Theo dữ liệu CrUX quý 2/2024, 68% trang có INP > 200ms đều gặp thrashing trên mobile — trong khi chỉ 22% trên desktop. Vì vậy, tối ưu thrashing là bước bắt buộc khi làm Technical SEO cho website đa nền tảng.