摘要:因為異步的關系中的回調函數并非立即執行,而是需要加入等待隊列中。通知繪制位圖到屏幕上而就只需要繪制圖層了,所以硬件加速的性能無疑更好。
寫在前面
說到web的高性能動畫,這部分內容其實已經是老生常談的了,不過其中還是有不少比較新的而且非常實用的內容可以和大家分享一下。
讀完這篇文章后相信大家都會對動畫渲染的機制以及制作60fps動畫的關鍵要素有足夠的理解,以后遇上了動畫相關的問題也可以很好的從源頭上解決。
動畫幀率可以作為衡量標準,一般來說畫面在 60fps 的幀率下效果比較好。
換算一下就是,每一幀要在 16.7ms (16.7 = 60/1000) 內完成渲染。因此,我們的首要任務是減少不必要的性能消耗。 越多的幀需要渲染的,意味著有越多的任務需要瀏覽器處理,所以掉幀就出現了,這是達到 60fps 的一個絆腳石。如果所有動畫都無法在 16.7ms 渲染完畢,不如考慮用略低的 30fps 幀率來渲染。
如何實現絲般順滑這里主要決定因素有二:
時機(Frame Timing): 新的一幀準備好的時機
成本(Frame Budget): 渲染新的一幀需要多長的時間
一般來說我們使用setTimeout(callback, 1/60)來實現16.7ms后執行動畫一幀的渲染。
然而setTimeout實際上并不準確。
首先,setTimeout依靠瀏覽器內置時鐘的更新頻率
例如:IE8及以前更新間隔為15.6ms,setTimeout(callback, 1/60)為16.7ms,那么它就需要兩個15.6ms才會觸發,這也意味著無故延遲了 15.6 x 2 - 16.7 = 14.5毫秒。
其次,假使能夠達到16.7ms,它還要面臨一個異步隊列的問題。
因為異步的關系setTimeout中的回調函數并非立即執行,而是需要加入等待隊列中。但問題是,如果在等待延遲觸發的過程中,有新的同步腳本需要執行,那么同步腳本不會排在timer的回調之后,而是立即執行。
function runForSeconds(s) { var start = +new Date(); while (start + s * 1000 > (+new Date())) {} } document.body.addEventListener("click", function () { runForSeconds(10); }, false); setTimeout(function () { console.log("Done!"); }, 1000 * 3);
以上的例子是,如果在等待觸發延遲的3秒過程中,有人點擊了body,那么回調還是準時在3s完成時觸發嗎?
實踐執行的時候,它會等待10s,同步函數總是優先于異步函數。
基于這些問題我們提出了另一個解決方案:requestAnimationFrame(callback)
window.requestAnimationFrame() 方法告訴瀏覽器您希望執行動畫并請求瀏覽器在下一次重繪之前調用指定的函數來更新動畫。該方法使用一個回調函數作為參數,這個回調函數會在瀏覽器重繪之前調用。-- MDN
當我們調用這個函數的時候,我們告訴它需要做兩件事:
我們需要新的一幀;
當你渲染新的一幀時需要執行我傳給你的回調函數
與 setTimeout 相比,rAF(requestAnimationFrame) 最大的優勢是由系統來決定回調函數的執行時機。具體一點講就是,系統每次繪制之前會主動調用 rAF 中的回調函數,如果系統繪制率是 60Hz,那么回調函數就每16.7ms 被執行一次,如果繪制頻率是75Hz,那么這個間隔時間就變成了 1000/75=13.3ms。換句話說就是,rAF 的執行步伐跟著系統的繪制頻率走。它能保證回調函數在屏幕每一次的繪制間隔中只被執行一次(函數節流,這篇文章就不細說了,感興趣的可以查一下),這樣就不會引起丟幀現象,也不會導致動畫出現卡頓的問題。
另外它可以自動調節頻率。如果callback工作太多無法在一幀內完成會自動降低為30fps。雖然降低了,但總比掉幀好。
同時對比使用 setTimeout 實現的動畫,當頁面被隱藏或最小化時,setTimeout 仍然在后臺執行動畫任務,由于此時頁面處于不可見或不可用狀態,刷新動畫是沒有意義的,而且還浪費 CPU 資源。而 rAF 則完全不同,當頁面處理未激活的狀態下,該頁面的屏幕繪制任務也會被系統暫停,因此跟著系統步伐走的 rAF 也會停止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了 CPU 開銷。
對于rAF的兼容性問題其實已經有了很好的處理方案了,以下是一種比較簡單的:
window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })();
這種寫法沒有考慮 cancelAnimationFrame 的兼容性,并且不是所有的設備繪制時間間隔都是1000/60。
這個是比較不錯的polyfil 。
總的來說,rAF解決了前面的第一個問題(繪制時機),至于第二個問題(繪制成本),rAF是無能為力的,最多也就是采取自動降低頻率的方式處理。
這里就需要從瀏覽器渲染方面來優化了,首先看下這個圖:
頁面首次加載時,瀏覽器會下載并解析 HTML,將 HTML 元素轉變為一個 DOM 節點的「內容樹」(content tree)。除此之外,樣式同樣會被解析生成「渲染樹」 (render tree)。為了提升性能,渲染引擎會分開完成這些工作,甚至會出現渲染樹比 DOM 樹更快生成出來。
在這個階段里最影響繪制時間的自然就是Layout了
// animation loop function update(timestamp) { for(var m = 0; m < movers.length; m++) { // DEMO 版本 //movers[m].style.left = ((Math.sin(movers[m].offsetTop + timestamp/1000)+1) * 500) + "px"; // FIXED 版本 movers[m].style.left = ((Math.sin(m + timestamp/1000)+1) * 500) + "px"; } rAF(update); }; rAF(update);
上面例子里DEMO版本是非常慢的,之所以慢的原因是,在修改每一個物體的left值時,會請求這個物體的offsetTop值,觸發了重排,這是一個非常耗時的reflow操作。
通常我們會不知不覺中寫了很多的頻繁layout的代碼,例如:
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 會導致「強制同步布局」(forced synchronous layouts),不過在技術發展過程中它演變成了更形象的詞 — 「布局抖動」(layout thrashing)(詳情可以看一下這篇文章 layout thrashing)。
瀏覽器會追蹤「臟元素」,在合適的時候將變換過程儲存起來,然后在讀取了特定屬性以后,開發者可以強制瀏覽器提前計算,這樣反復的讀寫會導致重排。
所以這里我們需要進行優化,先讀后寫就是一個解決方案,上面的代碼可以改寫為:
// Read var h1 = element1.clientHeight; var h2 = element2.clientHeight; var h3 = element3.clientHeight; // Write element1.style.height = (h1 * 2) + "px"; element2.style.height = (h2 * 2) + "px"; element3.style.height = (h3 * 2) + "px";
當然這種只能應對一些普通的情況,如果代碼是解耦的或者更復雜的讀寫后嵌套讀寫操作的這些情況可以使用一些比較成熟的解決方案,例如fastdom.js。另外一個小技巧是使用rAF來延遲全部的寫操作到下一幀執行也是很不錯的解決方案。
Paint生成布局后,瀏覽器將頁面繪制到屏幕上。這個環節和前一個步驟類似,瀏覽器會追蹤臟元素,將它們合并到一個超大的矩形區域中。每一幀內只會發生一次重繪,用于繪制這個被污染區域。
這個階段對性能的影響主要在于重繪。
例如,gif圖即使不可見,也可能導致paint,不需要時應將gif圖的display屬性設為none
在經常paint的區域,要避免代價太高的style
代價比較高的樣式:
color,border-style,visibility,background, text-decoration,background-image, background-position,background-repeat outline-color,outline,outline-style border-radius,outline-width,box-shadow background-size
參考網站:https://csstriggers.com/
為引起大范圍Paint的元素生成獨立的Layer以減小Paint的范圍
可以參考一下這個demo網站,綠色部分為重繪區域:
將所有繪制好的元素進行復合。
默認情況下,所有元素將會被繪制到同一個層中,如果將元素分開到不同的復合層中,更新元素對性能友好,不在同一層的元素不容易受到影響。
這一階段里CPU 繪制層,GPU 生成層。GPU 復合層上的改變代價最小性能消耗最少。所以這里的優化主要就是把代價高的改動都放到GPU上,也就是一般說的開啟硬件加速技術,可以說有益無害,如果設備的性能足夠開啟就對了。
這里的限制主要有:GPC和CPU之間帶寬,GPU的限度。
這里需要區分一下CPU,GPU的工作:
CPU工作比較多,還分主線程和合成線程。
主線程主要負責:
Javascript 的計算與執行
CSS 樣式計算
Layout 計算
將頁面元素繪制成位圖(paint),也就是光柵化(Raster)
將位圖給合成線程
合成線程則主要負責:
將位圖(GraphicsLayer 層)以紋理(texture) 的形式上傳給 GPU(GPC和CPU之間帶寬)
計算頁面的可見部分和即將可見部分(滾動)
CSS 動畫處理(CSS 動畫而言,由于其流程不受主線程的影響,所以性能更好。)
通知 GPU 繪制位圖到屏幕上
而GPU就只需要繪制圖層了,所以硬件加速的性能無疑更好。
開啟硬件加速的方式主要有:
通過改變 opacity 和 transform 的值觸發
通過transform的3D屬性強制開啟GPU加速
will-change顯式地通知瀏覽器對某一個元素的某個或某些元素做渲染優化
硬件加速之后,瀏覽器會為此元素多帶帶創建一個“層”。當有多帶帶的層之后,此元素的repaint操作將只需要更新自己,不用影響到別人。你可以將其理解為局部更新。所以開啟了硬件加速的動畫會變得流暢很多
默認情況下,transform、opacity這類css屬性CPU是直接通知GPU來做處理的,因為GPU能快速對texture(紋理:CPU傳輸到GPU的一個Bitmap)進行偏移、縮放、旋轉、修改透明度等操作,不經過主線程的layout、paint過程。也就是開啟了硬件加速。
will-change是個新事物,它能夠顯式地通知瀏覽器對某一個元素的某個或某些元素做渲染優化。 will-change 接收各種各樣的屬性值,比如一個或多個 CSS 屬性 (transform, opacity)、contents 或者 scroll-position。不過最常用值可能就是 auto,這個值表示的是瀏覽器將進行默認的優化:
GPU雖然擅長處理圖像,但是它也有瓶頸。連接CPU和GPU之間的帶寬是有限的,如果一次更新的層太多,則很容易就達到GPU的瓶頸,影響到動畫的流暢度。所以我們需要控制層的數量和層paint的次數。
控制層的數量可以理解,因為層的創建和更新都會消耗內存。而控制層paint的次數,是為了減少位圖更新的次數。每次位圖更新,合成線程就需要提交新的位圖給GPU。頻繁地更新位圖也會拖慢GPU的效率。
優化有度,我們總能聽到關于「復合層過多反而阻礙渲染」的討論。因為瀏覽器已經為優化做了能做的一切, will-change 的性能優化方案本身對資源要求很高。如果瀏覽器持續在執行某個元素的 will-change,就意味著瀏覽器要持續對這個元素的進行優化,性能消耗造成頁面卡頓。過多的復合層降低頁面性能的現象在移動端很常見。
避免意外生成的layer
z-index高于Layer的元素,也會生成多帶帶的Layer
demo以及說明頁面
實現絲般順滑主要決定因素有二:
時機(Frame Timing):
rAF
成本(Frame Budget):
避免layout:先讀后寫
盡量少paint:注意樣式的使用
適當的硬件加速
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97322.html
摘要:它和前端動畫之間沒有包含與被包含的關系,更不能將它們混為一談,只有兩者的有機結合才能創建出炫酷的界面。 第一次在segmentfault上發文章 :),歡迎評論指正。原文最初發表在 https://github.com/WarpPrism/... 動畫相關概念 幀:動畫過程中每一個靜止的狀態,每一張靜止的圖片 幀率:刷新頻率,每秒鐘播放的幀數,FPS(frame per second...
摘要:它和前端動畫之間沒有包含與被包含的關系,更不能將它們混為一談,只有兩者的有機結合才能創建出炫酷的界面。 第一次在segmentfault上發文章 :),歡迎評論指正。原文最初發表在 https://github.com/WarpPrism/... 動畫相關概念 幀:動畫過程中每一個靜止的狀態,每一張靜止的圖片 幀率:刷新頻率,每秒鐘播放的幀數,FPS(frame per second...
摘要:因此,如果可能,最好利用好毫秒響應預先計算開銷大的工作,這樣網站就更有可能實現的性能。空閑主線程工作分成不大于毫秒的塊。點擊按鈕見圖示,會在頁面運行時捕獲性能指標。 前言 經常能在博客或者論壇上看到很多有關前端性能優化的文章,但是卻很少看到如何分析一個網頁的性能的文章。到底什么樣的指標(或者說是標準)代表這個網頁性能好或者不好,通過什么方式來得到這些指標呢?因此,本文來講述下如何分析一...
閱讀 3242·2021-10-21 17:50
閱讀 3253·2021-10-08 10:05
閱讀 3379·2021-09-22 15:04
閱讀 580·2019-08-30 14:00
閱讀 1938·2019-08-29 17:01
閱讀 1507·2019-08-29 15:16
閱讀 3218·2019-08-26 13:25
閱讀 851·2019-08-26 11:44