Fix lỗi Force layout, reflow ảnh hưởng tới performance Frontend

Bài viết được sự cho phép của tác giả Thanh Lê

Tại sao nên đọc bài này?

  • Tìm hiểu xem Force Layout, reflow là gì?
  • Cách khắc phục, work around

Force layout

Force layout

Force layout/reflow là mỗi point ảnh hưởng cực lớn tới performance của website (Đặc biệt là mấy trang web phức tạp), mà nguyên nhân tạo ra nó mình thấy khá là… chí mạng. Vài dòng code cơ bản thôi mà lại khiến hậu quả lớn đến vậy!?

Như ví dụ trên hình trên là kết quả Performance check của CoinMarketCap, thời gian Hydrate là 1.03s, và trong đó, task force layout/reflow đã chiếm đâu đó khoảng 0.2s rồi (Tức là 20%)

Sau khi tìm hiểu rồi debug các kiểu thì mình nhận ra là issue này xuất phát từ một dòng lệch cực kì xàm

if (window.innerWidth < MOBILE_SIZE) {

Vậy cụ thể lỗi trên là như nào?

Những lỗi trên gọi là Layout Thrashing

Layout Thrashing means: Forcing the browser to calculate a layout that is never rendered to the screen.

Hiểu cơ bản là, nếu bạn dùng JS truy cập vào những thuộc tính liên quan tới layout thì thằng Browser sẽ phải tính toán lại data của layout đó để trả về cho bạn. Và công việc này khá là tốn resource CPU

Hồi xưa thì mình nghĩ là mọi con số về layout đều đã được tính toán và ready bởi trình duyệt rồi, kiểu như div này width bao nhiêu, height bao nhiêu thì render ra ngoài rồi phải nắm chứ nhỉ, vậy mà khi JS access vô thì nó không có mà phải tính lại ?!?!

Hmm, kì quá ta, vậy khi nào thì bị hiện tượng này? Chẳng lẽ avoid dùng mấy cái như window.innerWidth đồ luôn?

Layout Thrashing happens, when you request layout information of an element or the document, while layout is in an invalidated state.

// any DOM or CSSOM change flags the layout as invalid
document.body.classList.add('foo');

// reads layout == forces layout calculation
const box = element.getBoundingClientRect();

// write/mutate
document.body.appendChild(someBox);

//read/measure
const color = getComputedStyle(someOtherBox).color;

Rồi về cơ bản nếu bạn mutate DOM với những thứ liên quan tới Layout sẽ khiến cho layout invalid, và tiếp theo read các thuộc tính tới layout thì sẽ cần đợi thằng Browser render lại rồi mới trả số cho bạn được.

Như ví dụ trên document.body.classList.add('foo'); là một lệnh làm thay đổi layout, và đo đó, khi element.getBoundingClientRect(); run đoạn này sẽ cần đợi Browser tính toán lại layout mới rồi mới trả số được.

Tụi Browser đơn giản chỉ muốn thì thầm vào tai bạn

Fuck off , đừng có đụng vào cây DOM của tao. Tụi mày nên tôn trọng và tự sửa code lại cho hợp lý đi nhá

Vậy là cái issue ở trên mình gặp là mình đang read window.innerWidth mà chắc chắn trước đó đã có thằng nào mutate layout rồi.

  Thuật toán frontend: Tìm node chứa content chính

  Tổng hợp các thuật ngữ trong Frontend bạn nhất định phải biết!

Sửa sao cho vừa lòng Browser?

Cơ bản bạn sẽ cần tách ra 2 phần khi xử lý DOM

  • Read layout
  • Mutate DOM
// reads layout
const box = element.getBoundingClientRect();
const color = getComputedStyle(someOtherBox).color;

// write
document.body.classList.add('foo');
document.body.appendChild(someBox);

Read Layout sẽ không làm thay đổi gì thằng DOM cả, do đó chúng ta nên read nó đầu tiên, rồi xong sau mới bảo tụi Browser là ok, xong rồi, giờ mutate cái DOM hộ bố cái!

Ngoài ra với các tình huống phức tạp thì mình có thể dùng thư viện, cụ thể là Fastdom

import fastdom from 'fastdom';

function resizeAllParagraphsToMatchBoxWidth(paragraphs, box) {

    fastdom.measure(() => {
        const width = box.offsetWidth;

        fastdom.mutate(() => {
            for (let i = 0; i < paragraphs.length; i++) {
                paragraphs[i].style.width = width + 'px';
            }
        });
    });
}

Tham khảo việc làm Front-End hấp dẫn trên TopDev

Lifecycle của một frame

Ok giờ chuyên sâu hơn một xíu

Lifecycle của một frame
Mỗi lần vsync nghĩa là máy chúng ta sync data lên màn hình, cái này tương đương với một khung hình mới đó

Lifecycle của một frame

Lifecycle của một frame

Ròi, ngắn gọn là vầy:

  1. Nhận input event từ user. Có thể là click, scroll, touch,…
  2. JS xử lý đống event đó
  3. requestAnimationFrameObserver callback chạy (nếu có)
  4. Sau khi mọi thứ đủ đầy, commit mọi thứ ra màn hình – nghĩa là mapping những thứ đã tính toán được (Div này width bao nhiêu, height bao nhiêu, position ở đâu, bla bla) ra màn hình máy tính của user

Bằng cách hiểu cái life cycle như vậy, chúng ta tránh việc Layout Thrashing bằng cách:

Thay vì vừa read vừa write layout
Thay vì vừa read vừa write layout
Đưa cái đoạn write vào requestAnimationFrame
Đưa cái đoạn write vào requestAnimationFrame

Cách debug

Cái này thì cũng khá đơn giản. Bạn bật Web debug tool lên, ấn vào tab Performance, set cái CPU slow xuống x6 (Nếu máy bạn mạnh). Rồi Start Profiling

Cách debug

Nằm chờ, sau đó coi trong đó có cái thằng nào màu tím góc trên hiện màu đỏ như hình ở đầu bài không nhé.

Đây là list các thuộc tính có liên quan tới Layout Thrashing: https://gist.github.com/paulirish/5d52fb081b3570c81e3a

Cách workaround

Rồi, nói chung nếu muốn fix theo cách chính thống thì mình đã nói ở trên là tách vụ Read layout và Mutate layout riêng biệt và nhét vào rAF. Tuy nhiên nhiều khi nó cũng phức tạp và mệt mỏi, thì mình có một tip nhỏ để workaround.

Ở JS head mình đọc Layout data đó và lưu vào một biến cache, VD

window.widthCached = window.innerWidth

Rỗi giờ chỗ nào read window.innerWidth thì thay bằng window.widthCached là xong. Vậy là solve được vấn đề mình gặp phải ở đâu bài ‍

Đương nhiên là cái idea này không thể apply được cho mọi trường hợp, chỉ có những thuộc tính gần như là ít thay đổi, và ready lúc mình cần lưu cache thì mới chạy được thôi. Tuy nhiên work-around thì tùy vào năng lực sáng tạo của bạn mà.

Bài viết gốc được đăng tải tại thanhle.blog

Xem thêm:

Tham khảo ngay việc làm IT mọi cấp độ trên TopDev!