預防“佈局抖動”
佈局抖動是因 JavaScript 的 DOM 元素被多被次暴力寫,然後讀,導致文件重排而出現的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 讀 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元素的讀寫操作放在一起執行。這意味著文件只需重排一次即可。
1 2 3 4 5 6 7 8 9 10 11 |
// 讀 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的讀操作在當前同步狀態。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 讀 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時間軸可看出,有多個佈局抖動穿插其中。
Before | After |
---|---|
在改用requestAnimationFrame
後,僅僅只觸發一次佈局事件,其結果是操作快了約96%。
它具有伸縮性嗎?
在一個簡單案例裡,使用requestAnimationFrame
來延遲DOM寫操作,從而大大提高效能,但這項技術沒有伸縮性可言。
在我們的應用中,可能需要在DOM元素上執行先寫後讀操作,然後再次掉入佈局抖動的坑,只是在不同幀。
1 2 3 4 5 6 7 8 9 10 |
// 讀 var h1 = element1.clientHeight; // 寫 requestAnimationFrame(function() { element1.style.height = (h1 * 2) + 'px'; // 我們可能想在設定高度後再讀取新高度值。 var height = element1.clientHeight; }); |
我們可以將讀操作放到另外一個requestAnimationFrame
,但我們不能保證應用程式的另一部分,沒有把寫操作放在同一幀上。
介紹 ‘FastDom’
FastDom是一個輕量的庫,它提供一個公共介面,能讓DOM的讀/寫操作捆綁在一起。其實,它就是利用上述同樣的 requestAnimationFrame
技術來大大提高DOM操作速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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案例
完善FastDom
web應用缺少一個明確的方式,來解決佈局抖動問題。正如一個應用程式很難協調所有不同的部分,來確保產品最終是高效的。如果FastDom能為開發者們提供一個簡單介面來解決這個問題,那隻能意味著它是個好東西。
瞧一瞧 FastDom 專案,歡迎隨時通過 pull requests 或 filing issues 來完善它。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式