避免 Layout 帶來的痛 #譯

本文翻譯於《Preventing ‘layout thrashing’》,該文介紹 DOM 操作導致卡頓的原因,並且進一步介紹 requestAnimationFrame 與 FastDOM 如何解決這個痛點。

realdennis
8 min readJun 14, 2019

當我們透過 JavaScript 劇烈的操作 DOM 節點,「寫然後讀」往往會發生 Layout Thrashing:

// Read 
var h1 = element1.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
// Read (triggers layout)
var h2 = element2.clientHeight;
// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';
// Read (triggers layout)
var h3 = element3.clientHeight;
// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';

DOM 節點被被寫入新的 height 時,會使得頁面 reflow,瀏覽器非常懶,會等到所有操作結束或是這一幀(Frame)結束後才會去 reflow。

但是當我們在瀏覽器 reflow 之前就去讀取新的值出來時,我們會迫使瀏覽器提前 reflow ,這稱之為「強制同步佈局」(forced synchonous layout)。

發生強制同步佈局在現在的桌面瀏覽器看來,其副作用可能不是那麼顯而易見,但是對效能較差的設備、移動裝置造成的效能影響卻相當劇烈。

如何快速解決?

在理想的狀況下,我們可以透過重新排序上述的操作來解決這個問題,批次的讀取與批次的寫入(Batch),這樣我們就只需要 reflow 一次。

// Read 
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
// Document reflows at end of frame

(譯注:如果你各位修過計算機結構,是不是覺得有點耳熟呢?pipeline…)

但是在現實的狀況呢?

實際上這並沒有那麼簡單。

大型應用程式中,程式碼遍佈於每個地方,所有的程式碼都能輕而易舉的操作DOM,我們不會輕易的(也不應該)將本來解耦、拆分地漂亮的程式碼給重新修改、排序,導致程式碼耦合

那我們該做些什麼讓程式碼批次讀寫以提高程式的性能呢?

requestAnimationFrame

window.requestAnimationFrame 會將回呼函數排程於下一幀執行,這就類似於 setTimeout(fn,0)

這非常有用,因為我們可以使用它來安排所有DOM寫入操作在下一幀中一起執行,使得所有的讀取操作在當前能夠同步執行

// Read 
var h1 = element1.clientHeight;
// Write
requestAnimationFrame(function() {
element1.style.height = (h1 * 2) + 'px';
});
// Read
var h2 = element2.clientHeight;
// Write
requestAnimationFrame(function() {
element2.style.height = (h2 * 2) + 'px';
});

(譯注:這樣程式碼與一開始的結構相似,並且由於 write 操作都被非同步排程至下一個 frame ,所以可以看成是這個樣子:讀讀讀、(下一幀)、寫寫寫。)

這表示我們可以將程式碼做小量改動的情況下,將昂貴的 DOM 操作批次處理掉,讚👍。

Working Example

我(原文作者)建立一個例子來做概念性驗證,你可以從瀏覽器的Timeline中看到 Layout Thrashing 。

Before
After

我們透過 requestAnimationFrame 的小量改動後,只發生一次 reflow ,因此操作的速度上升了幾乎96%

Can this scale?

在簡單的例子裡頭,使用 requestAniamtionFrame 確實允許我們推遲了DOM的寫入操作並大大提升程式性能,不過這並不能擴展

在 App 裡,我們可能需要在完成寫入後又做讀取,結果顯而易見的 — 再次遭遇了 Layout Thrashing,只是發生在不同幀罷了。

// Read 
var h1 = element1.clientHeight;
// Write
requestAnimationFrame(function() {
element1.style.height = (h1 * 2) + 'px';
// We may want to read the new
// height after it has been set
var height = element1.clientHeight;
});

儘管我們也可以把讀取操作透過 requestAnimationFrame 推遲到下一幀中,但是我們無法保證程式的另一部分也把寫入操作推到下一幀中

基本上它會變得相當混亂,而你不再能控制 DOM 讀寫的時間…。

介紹 FastDOM

FastDOM 是原文作者所編寫的一個小型函式庫,它提供一個用於協調批次讀寫 DOM 的簡單接口,它使用上述類似的 requestAnimationFrame 技術來大量加速 DOM 操作。

fastdom.read(function() {   
var h1 = element1.clientHeight;
fastdom.write(function() {
element1.style.height = (h1 * 2) + 'px';
});
});
fastdom.read(function() {
var h2 = element2.clientHeight;
fastdom.write(function() {
element2.style.height = (h1 * 2) + 'px';
});
});

(譯注:如果你夠細心的話會發現現在的文檔已經將 read、write 改成 measure 與 mutate 了,詳細請看點擊該 GitHub 專案連結。)

FastDOM 透過協調寫入、讀取操作,並在下一幀對於它們批次處理(讀然後寫)。這意味著你可以在自己的組件中使用,且不用擔心是否影響外部組件。

使用 FastDOM 的意義為何?

透過使用 FastDOM ,你可以使所有的DOM操作非同步,這也意味著你無法始終假設該 DOM 節點處於什麼狀態,以前的同步操作,在現在看來可能是還沒完成的。

(譯注:其實就是把巨量的操作非同步化,使用起來類似於 callback hell 的概念,另外, FastDOM 其實也有 promise-based 的 API 。)

我們雖然增加了一些必須編寫的程式碼(這裡是指使用 FastDOM),來達成相同的工作量。就個人而言,我認為是一個很小的代價,但能顯著提升效能。

改善FastDom

前端應用缺乏解決 Layout Thrashing 的明確方法。

隨著應用程式的增長,協調所有不同的部分變得相當困難,為了確保最終產品的效能,如果 FastDom 可以幫助開發人員提供一個簡單的界面來解決這個問題,表示 FastDOM 的存在是好事。

Have a look at the FastDom project and feel free to contribute by submitting pull requests or filing issues.

(譯注:這我就不翻啦,反正就是透過 PR、issue 來幫助該專案繼續成長。)

Hashtag

#reflow #repaint #效能優化 #batch_update #requestAnimationFrame #DOM #fastdom

Photo by Matt Hardy on Unsplash

--

--

No responses yet