Layout Thrashing: Kẻ thù thầm lặng khiến trang web của bạn "giật lag" và cách khắc phục

Layout Thrashing: Kẻ thù thầm lặng khiến trang web của bạn "giật lag" và cách khắc phục

Layout Thrashing là gì?

Bạn đã bao giờ thấy trang web của mình bị giật, lag nhẹ khi cuộn hoặc tương tác chưa? Rất có thể, thủ phạm ẩn mình đằng sau chính là Layout Thrashing.

Để hiểu Layout Thrashing, chúng ta cần nhớ lại cách trình duyệt vẽ trang web. Về cơ bản, quá trình này diễn ra qua các bước chính:

  • Layout (Reflow): Trình duyệt tính toán vị trí và kích thước của tất cả các phần tử trên trang.
  • Paint: Trình duyệt điền màu sắc, hình ảnh, văn bản cho các phần tử.
  • Composite: Trình duyệt ghép các lớp lại với nhau để tạo thành hình ảnh cuối cùng hiển thị trên màn hình.

Layout Thrashing xảy ra khi bạn liên tục đọc và ghi các thuộc tính DOM có liên quan đến bố cục (layout) trong một vòng lặp hoặc trong một khoảng thời gian ngắn. Mỗi lần bạn đọc một thuộc tính bố cục sau khi đã thay đổi nó, trình duyệt sẽ bị buộc phải thực hiện lại bước Layout một cách đồng bộ (synchronously) để đảm bảo bạn nhận được giá trị chính xác nhất. Việc này lặp đi lặp lại nhiều lần sẽ gây ra "giật lag" rõ rệt, đặc biệt trên các thiết bị có cấu hình yếu.

Vì sao Layout Thrashing lại là vấn đề?

Hãy hình dung bạn đang trang trí lại một căn phòng. Bạn di chuyển một chiếc ghế (thay đổi kích thước/vị trí), sau đó bạn muốn biết chính xác khoảng cách từ chiếc ghế đó đến bức tường. Ngay lập tức bạn đo lại. Rồi bạn lại di chuyển chiếc ghế một chút, và lại đo. Cứ thế lặp đi lặp lại. Công việc sẽ chậm đi rất nhiều!

Trong trình duyệt, mỗi lần Layout là một tác vụ nặng nề. Việc ép trình duyệt phải tính toán lại bố cục nhiều lần trong một khung hình (frame) sẽ tiêu tốn tài nguyên CPU và làm giảm tốc độ khung hình (FPS), dẫn đến trải nghiệm người dùng kém mượt mà.

Những "kẻ tình nghi" gây ra Layout Thrashing

Layout Thrashing thường xảy ra khi bạn xen kẽ các thao tác đọc và ghi vào DOM. Dưới đây là một số thuộc tính khi đọc (read) hoặc ghi (write) có thể gây ra Layout/Reflow:

Thuộc tính khi đọc (Read) có thể kích hoạt Layout:

  • offsetHeight, offsetWidth, clientHeight, clientWidth
  • scrollWidth, scrollHeight, offsetTop, offsetLeft, scrollTop, scrollLeft
  • getComputedStyle() (khi truy cập các thuộc tính liên quan đến bố cục)
  • getBoundingClientRect()

Thuộc tính khi ghi (Write) có thể kích hoạt Layout:

  • width, height, left, top, margin, padding, border, border-width
  • display, position, float
  • font-size, text-align, vertical-align
  • ... và nhiều thuộc tính CSS khác liên quan đến hình dạng, kích thước, vị trí của phần tử.

Cách phòng tránh Layout Thrashing hiệu quả

May mắn thay, có nhiều cách để "thuần hóa" Layout Thrashing và giữ cho trang web của bạn luôn mượt mà:

1. Gom nhóm các thao tác đọc và ghi DOM (Batch DOM reads and writes)

Đây là nguyên tắc vàng. Thay vì xen kẽ, hãy thực hiện tất cả các thao tác đọc DOM trước, sau đó mới thực hiện tất cả các thao tác ghi DOM. Điều này giúp trình duyệt có thể tối ưu hóa các lần Layout.

// BAD: Layout Thrashing occursconst element = document.getElementById('myElement');element.style.width = '100px'; // Write (causes layout invalidation)const width = element.offsetWidth; // Read (forces synchronous layout)element.style.height = '50px'; // Writeconst height = element.offsetHeight; // Read (forces synchronous layout again)// GOOD: Batch reads and writesconst element = document.getElementById('myElement');// All reads firstconst initialWidth = element.offsetWidth;const initialHeight = element.offsetHeight;// Then all writeselement.style.width = (initialWidth + 10) + 'px';element.style.height = (initialHeight + 5) + 'px';

2. Sử dụng requestAnimationFrame cho các thay đổi DOM liên tục

Khi bạn cần thực hiện các thay đổi DOM liên tục, đặc biệt là trong animation, hãy sử dụng requestAnimationFrame(). Hàm này yêu cầu trình duyệt thực hiện callback trước lần vẽ tiếp theo, cho phép trình duyệt nhóm các thay đổi và thực hiện Layout một cách hiệu quả hơn.

function animateElement() {  const element = document.getElementById('myElement');  // Read properties  const currentLeft = element.offsetLeft;  // Perform calculations and then write  element.style.left = (currentLeft + 1) + 'px';  if (currentLeft < 300) {    requestAnimationFrame(animateElement);  }}requestAnimationFrame(animateElement);

3. Ưu tiên các thuộc tính CSS không gây Layout (Non-layout triggering CSS properties)

Đối với các hiệu ứng animation, hãy cố gắng sử dụng các thuộc tính CSS không kích hoạt Layout hoặc Paint, như transformopacity. Các thuộc tính này chỉ ảnh hưởng đến bước Composite, giúp animation mượt mà hơn rất nhiều.

/* Thay vì dùng left/top */.animate-me {  position: relative;  transition: transform 0.3s ease-out;}.animate-me.moved {  transform: translateX(100px) translateY(50px);}

4. Tận dụng CSS hiện đại (Flexbox, Grid) và Virtualization

  • Flexbox và Grid: Các module bố cục hiện đại này được tối ưu hóa tốt hơn cho hiệu năng so với các phương pháp cũ (như float hay table layout) và thường ít gây ra Layout Thrashing hơn khi thay đổi nội dung.
  • Virtualization (Windowing): Đối với các danh sách dài, chỉ render các phần tử hiện đang hiển thị trên màn hình. Điều này giúp giảm đáng kể số lượng phần tử mà trình duyệt phải quản lý và tính toán bố cục.

Kết luận

Layout Thrashing không phải là một lỗi của trình duyệt, mà là một hành vi tự nhiên khi bạn yêu cầu thông tin bố cục đã bị thay đổi. Nắm vững nguyên nhân và các biện pháp phòng tránh là chìa khóa để xây dựng những ứng dụng web không chỉ đẹp mắt mà còn mượt mà, phản hồi nhanh, mang lại trải nghiệm tuyệt vời cho người dùng. Hãy luôn "batch" các thao tác DOM của bạn và tận dụng tối đa sức mạnh của requestAnimationFrame và CSS hiện đại nhé!

Layout Thrashing: Kẻ thù thầm lặng khiến trang web của bạn "giật lag" và cách khắc phục | Dev Chia Sẻ