Bạn đã bao giờ tự hỏi tại sao ứng dụng web của mình đôi khi lại "giật, lag" một cách khó hiểu, đặc biệt là khi có nhiều tương tác với DOM? Rất có thể, bạn đang gặp phải một "kẻ phá bĩnh" quen mặt trong giới lập trình web: Layout Thrashing (hay còn gọi là Reflow Thrashing).
Trong bài viết này, chúng ta sẽ cùng "giải mã" hiện tượng này, hiểu rõ nguyên nhân và trang bị những "vũ khí" lợi hại để đảm bảo website của bạn luôn mượt mà như lụa.
Layout Thrashing là gì?
Để hiểu Layout Thrashing, chúng ta cần nắm sơ qua cách trình duyệt "vẽ" trang web của bạn. Quy trình rendering cơ bản của trình duyệt diễn ra theo các bước chính:
- Style: Tính toán các quy tắc CSS cho từng phần tử.
- Layout (Reflow): Tính toán vị trí và kích thước chính xác của tất cả các phần tử trên trang. Đây là bước "nặng đô" nhất vì nó có thể ảnh hưởng đến các phần tử khác.
- Paint: "Vẽ" các pixel của các phần tử lên màn hình (ví dụ: màu sắc, đường viền, bóng đổ).
- Composite: Ghép các layer đã vẽ lại với nhau theo đúng thứ tự để tạo ra hình ảnh cuối cùng.
Layout Thrashing xảy ra khi bạn liên tục thay đổi kiểu dáng của một phần tử (ví dụ: thay đổi width, height, left, top, display, font-size...) và ngay lập tức yêu cầu trình duyệt cung cấp thông tin về bố cục của phần tử đó (ví dụ: offsetWidth, offsetHeight, getBoundingClientRect()).
Khi bạn thay đổi một thuộc tính CSS ảnh hưởng đến bố cục, trình duyệt sẽ đánh dấu trang cần được "bố trí lại". Nếu sau đó bạn ngay lập tức đọc một thuộc tính liên quan đến bố cục, trình duyệt sẽ bị BUỘC phải thực hiện bước Layout (Reflow) đồng bộ ngay lập tức để cung cấp cho bạn giá trị chính xác. Việc này lặp đi lặp lại trong một vòng lặp hoặc chuỗi thao tác nhanh chóng sẽ tạo ra một "cuộc chiến" bố cục, làm giảm hiệu năng nghiêm trọng.
Tại sao Layout Thrashing lại là "ác mộng"?
Tưởng tượng bạn đang xây một ngôi nhà. Mỗi khi bạn thay đổi vị trí một bức tường (thay đổi bố cục), bạn lại ngay lập tức hỏi "chiều rộng căn phòng bây giờ là bao nhiêu?". Người thợ (trình duyệt) sẽ phải dừng mọi việc, đo lại toàn bộ căn phòng rồi mới trả lời bạn. Nếu bạn cứ lặp đi lặp lại hành động này, công trình sẽ tiến triển rất chậm và tốn kém.
Trong trình duyệt, mỗi lần Layout/Reflow là một thao tác tốn tài nguyên. Nếu nó xảy ra quá nhiều lần trong một khung hình (frame), trang web của bạn sẽ bị "đứng hình" hoặc giật cục (janky), mang lại trải nghiệm rất tệ cho người dùng.
Những "kẻ khơi mào" Layout Thrashing phổ biến
Layout Thrashing thường xảy ra khi bạn thực hiện một chuỗi thao tác "đọc-ghi" DOM xen kẽ. Cụ thể, các thuộc tính ghi ảnh hưởng đến bố cục bao gồm:
width,height,margin,padding,borderleft,top,right,bottom(khi sử dụngposition: absolutehoặcrelative)font-size,line-heightdisplay,float,clear- Thêm/xóa phần tử DOM
Và các thuộc tính đọc kích hoạt reflow đồng bộ khi được gọi ngay sau một thao tác ghi:
offsetWidth,offsetHeight,clientWidth,clientHeightscrollWidth,scrollHeight,scrollTop,scrollLeftgetComputedStyle()getBoundingClientRect()clientTop,clientLeft
Ví dụ minh họa một đoạn code gây Layout Thrashing:
function badAnimation() { const element = document.getElementById('myElement'); for (let i = 0; i < 100; i++) { // Ghi: thay đổi chiều rộng element.style.width = (i * 2) + 'px'; // Đọc: ngay lập tức lấy chiều rộng tính toán // Điều này buộc trình duyệt phải layout lại đồng bộ ở MỖI lần lặp console.log(element.offsetWidth); }}badAnimation();Cách "đánh bại" Layout Thrashing: Những chiến lược hiệu quả
May mắn thay, có nhiều cách để tránh "cuộc chiến" bố cục này:
1. Tách biệt các thao tác ĐỌC và GHI DOM
Đây là nguyên tắc vàng! Hãy gom tất cả các thao tác ghi (thay đổi style) vào một khối, sau đó gom tất cả các thao tác đọc (lấy giá trị bố cục) vào một khối khác.
Ví dụ:
function goodAnimation() { const element = document.getElementById('myElement'); const widths = []; // Bước 1: Thực hiện tất cả các thao tác GHI for (let i = 0; i < 100; i++) { element.style.width = (i * 2) + 'px'; // Lưu ý: Không đọc offsetWidth ở đây! } // Bước 2: Sau đó, thực hiện tất cả các thao tác ĐỌC // Trong trường hợp này, việc đọc sau khi vòng lặp kết thúc sẽ chỉ kích hoạt 1 reflow cuối cùng. // Nếu bạn cần đọc giá trị sau mỗi lần thay đổi, bạn cần kết hợp với requestAnimationFrame. widths.push(element.offsetWidth); // chỉ đọc một lần cuối console.log(widths);}goodAnimation();Tuy nhiên, ví dụ trên có thể chưa đủ nếu bạn muốn thực hiện một chuỗi đọc-ghi trong một animation. Lúc đó, chúng ta cần requestAnimationFrame.
2. Sử dụng requestAnimationFrame
requestAnimationFrame (rAF) là API giúp trình duyệt tối ưu hóa các thay đổi DOM bằng cách thực hiện chúng ngay trước khi trình duyệt thực hiện chu kỳ vẽ tiếp theo. Nó đảm bảo các thao tác DOM của bạn được nhóm lại và thực hiện ở thời điểm tối ưu, tránh ép buộc reflow đồng bộ.
Ví dụ:
function animateSmoothly() { const element = document.getElementById('myElement'); let i = 0; function frame() { if (i < 100) { // GHI: Thay đổi style element.style.width = (i * 2) + 'px'; // ĐỌC: Lấy giá trị bố cục // Tuy nhiên, việc đọc này sẽ được thực hiện sau khi style được áp dụng // và trước khi frame tiếp theo được vẽ, giúp tránh thrashing. console.log(element.offsetWidth); i++; requestAnimationFrame(frame); // Yêu cầu frame tiếp theo } } requestAnimationFrame(frame); // Bắt đầu animation}animateSmoothly();Trong ví dụ này, mỗi thao tác đọc offsetWidth sẽ được thực hiện sau khi thay đổi width nhưng trong cùng một frame của trình duyệt, do đó trình duyệt chỉ cần thực hiện một lần layout cho mỗi frame, chứ không phải nhiều lần layout đồng bộ.
3. Ưu tiên các thuộc tính không gây Layout/Reflow
Khi animate, hãy ưu tiên các thuộc tính chỉ gây Paint hoặc Composite, thay vì Layout. Ví dụ điển hình là sử dụng transform và opacity.
- Thay vì thay đổi
left/topđể di chuyển, hãy dùngtransform: translate(x, y). - Thay vì thay đổi
width/heightđể thay đổi kích thước, hãy dùngtransform: scale(x).
Các thuộc tính này thường được xử lý trên GPU và không yêu cầu trình duyệt phải tính toán lại toàn bộ bố cục.
4. Sử dụng will-change
Thuộc tính CSS will-change cho phép bạn "mách" trình duyệt rằng một phần tử sẽ thay đổi một thuộc tính cụ thể trong tương lai gần. Trình duyệt có thể sử dụng thông tin này để tối ưu hóa, ví dụ như tạo một layer riêng cho phần tử đó, giúp các thay đổi sau này mượt mà hơn.
Ví dụ:
.my-animated-element { will-change: transform, opacity; /* Báo cho trình duyệt biết sẽ thay đổi transform và opacity */}Lưu ý: Không nên lạm dụng will-change vì nó có thể tiêu tốn tài nguyên nếu được áp dụng cho quá nhiều phần tử.
Kết luận
Layout Thrashing là một trong những nguyên nhân phổ biến gây ra hiệu năng kém trên các ứng dụng web phức tạp. Bằng cách hiểu rõ cơ chế hoạt động của trình duyệt và áp dụng các kỹ thuật như tách biệt đọc/ghi DOM, sử dụng requestAnimationFrame, ưu tiên các thuộc tính không gây reflow và tận dụng will-change, bạn có thể "thuần hóa" hiện tượng này và mang lại trải nghiệm người dùng mượt mà, chuyên nghiệp hơn. Hãy luôn giữ cho trình duyệt của bạn "hạnh phúc" bằng cách giảm thiểu những lần tính toán bố cục không cần thiết nhé!