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

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

notion image
 
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 fuk 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

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'; } }); }); }
 

Lifecycle của một frame

Ok giờ chuyên sâu hơn một xíu
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 đó
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 đó
notion image
 
notion image
 
Ròi, ngắn gọn là vầy:
  1. Nhận input event từ user. Có thể là click, scroll, touch,…
  1. JS xử lý đống event đó
  1. requestAnimationFrameObserver callback chạy (nếu có)
  1. 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
notion image
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
 

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à 😆
 

Nguồn

Hai nguồn chính mà mình tham khảo và lấy hình

Các bài viết “lan quyên”

 

Loading Comments...

Follow me @thanhledev

Xin lỗi các bạn vì thời gian qua mình không dành thời gian viết nhiều. Dạo này mình khá bận cho dự án https://getnimbus.io. Check it out 🥳