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/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
Ròi, ngắn gọn là vầy:
Nhận input event từ user. Có thể là click, scroll, touch,…
JS xử lý đống event đó
requestAnimationFrame
vàObserver
callback chạy (nếu có)
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
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
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
List các thuộc tính có liên quan tới Layout Thrashing
https://afarkas.github.io/layout-thrashing/#/
Các bài viết “lan quyên”
Server side rendering với Hydration lãng phí tài nguyên như thế nào?
Em thề là đọc 1 nửa ở mail phải vào đây cho 1 vote cái đã =))