Technical SEO

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.

3 lượt xem Cập nhật: 27/05/2026

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:

  1. JS đọc el.offsetHeight → trình duyệt phải layout ngay để trả kết quả đúng;
  2. JS thay đổi el.style.width = '200px' → trình duyệt lại cần layout lại để cập nhật;
  3. 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:

  1. Đọ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.
  2. 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ặc documentFragment.
  3. Sử dụng API hiện đại hơn: thay vì offsetTop, ưu tiên element.getBoundingClientRect() (đã được cache trong frame), hoặc dùng ResizeObserver để theo dõi thay đổi kích thước mà không cần polling.
  4. 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).
  5. 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.