預防“佈局抖動”
佈局抖動是因 JavaScript 的 DOM 元素被多被次暴力寫,然後讀,導致檔案重排而出現的。
// 讀
var h1 = element1.clientHeight;
// 寫(無效佈局)
element1.style.height = (h1 * 2) + ‘px’;
// 讀(觸釋出局)
var h2 = element2.clientHeight;
// 寫(無效佈局)
element2.style.height = (h2 * 2) + ‘px’;
// 讀(觸釋出局)
var h3 = element3.clientHeight;
// 寫(無效佈局)
element3.style.height = (h3 * 2) + ‘px’;
當DOM元素被寫入值,佈局就“無效”,而多次這樣就會導致檔案重排。瀏覽器很懶,它總想等到當前操作(或幀)的最後一步才重排。
然而,如果在當前操作(幀)完成前,從DOM元素中獲取值,這會迫使瀏覽器提早執行佈局操作,這稱為“強制同步佈局”,這可是效能殺手!
佈局抖動的副作用在現代桌面瀏覽器上並不明顯;但對於低配置的移動裝置來說,其後果就不堪設想了。
能快速修複?
在理想情況下,我們可能透過簡單地重覆執行,以至於將DOM元素的讀寫操作放在一起執行。這意味著檔案只需重排一次即可。
// 讀
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// 寫(無效佈局)
element1.style.height = (h1 * 2) + ‘px’;
element2.style.height = (h2 * 2) + ‘px’;
element3.style.height = (h3 * 2) + ‘px’;
// 檔案在最後一幀將進行重排
現實情況會怎麼樣?
現實情況並非如此簡單。大型應用程式的程式碼會分散到各個地方,因此這些地方都有危險的DOM操作。所以不能簡單地(絕對不應該)聚集它們,而需要解耦程式碼,只是需要控制好執行順序。那如何讓讀寫操作捆綁在一起,從而獲得最佳效能呢?
進入requestAnimationFrame
window.requestAnimationFrame是一個將操作安排在下一幀一起執行的函式,類似於setTimeout(fn, 0)。這是非常有用的,因為能使用它來安排所有DOM的寫操作在下一幀一起執行,保留所有DOM的讀操作在當前同步狀態。
// 讀
var h1 = element1.clientHeight;
// 寫
requestAnimationFrame(function() {
element1.style.height = (h1 * 2) + ‘px’;
});
// 讀
var h2 = element2.clientHeight;
// 寫
requestAnimationFrame(function() {
element2.style.height = (h2 * 2) + ‘px’;
});
這意味著我們能很好地封裝程式碼了。經過小小調整後的程式碼,就將高耗能的DOM操作捆綁在一起!實在太棒了!
工作實體
我建立了一個工作案例來證明這個觀點。從第一個截圖的chrome時間軸可看出,有多個佈局抖動穿插其中。
在改用requestAnimationFrame 後,僅僅只觸發一次佈局事件,其結果是操作快了約96%。
它具有伸縮性嗎?
在一個簡單案例裡,使用requestAnimationFrame來延遲DOM寫操作,從而大大提高效能,但這項技術沒有伸縮性可言。
在我們的應用中,可能需要在DOM元素上執行先寫後讀操作,然後再次掉入佈局抖動的坑,只是在不同幀。
// 讀
var h1 = element1.clientHeight;
// 寫
requestAnimationFrame(function() {
element1.style.height = (h1 * 2) + ‘px’;
// 我們可能想在設定高度後再讀取新高度值。
var height = element1.clientHeight;
});
我們可以將讀操作放到另外一個requestAnimationFrame ,但我們不能保證應用程式的另一部分,沒有把寫操作放在同一幀上。
介紹 ‘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’;
});
});
FastDom透過接收讀寫操作,併在下一幀捆綁它們(先讀後寫),從而消除DOM的相互影響。這意味著我們能獨立編寫應用程式元件,而不用擔心它們在應用程式中互相影響。
使用FastDom的啟示
透過使用FastDom,會讓所有DOM任務變成非同步,這意味著你不能總是假設DOM將會以什麼狀態進行操作。操作從之前的同步,變成現在的非同步方式。因此,可能沒執行完非同步處理函式就會執行下一步操作了。
要解決這一點,我打算用事件系統來明確操作何時完成,和明確依賴於完成後所做出的響應操作。
雖然所做工作是一樣的,但能透過增加程式碼量來顯著提高效能。我個人認為這個代價小。
FastDom案例
- Animation example:http://wilsonpage.github.io/fastdom/examples/animation.html
- Aspect ratio example:http://wilsonpage.github.io/fastdom/examples/aspect-ratio.html
完善FastDom
web應用缺少一個明確的方式,來解決佈局抖動問題。正如一個應用程式很難協調所有不同的部分,來確保產品最終是高效的。如果FastDom能為開發者們提供一個簡單介面來解決這個問題,那隻能意味著它是個好東西。
瞧一瞧 FastDom 專案,歡迎隨時透過 pull requests 或 filing issues 來完善它。
原文出處:wilsonpage.co.uk
譯文出處:伯樂線上 – 劉健超-J.c
連結:http://web.jobbole.com/82546/