相信很多人在作移動端動畫的時候遇到了卡頓的問題,這篇文章嘗試從瀏覽器渲染的角度;一點一點告訴你動畫優化的原理及其技巧,做爲你工做中優化動畫的參考。文末有優化技巧的總結。css
由於GPU合成沒有官方規範,每一個瀏覽器的問題和解決方式也不一樣;因此文章內容僅供參考。html
提升動畫的優化不得不說起瀏覽器是如何渲染一個頁面。在從服務器中拿到數據後,瀏覽器會先作解析三類東西:web
解析html,xhtml,svg這三類文檔,造成dom樹。ajax
解析css,產生css rule tree。chrome
解析js,js會經過api來操做dom tree和css rule tree。canvas
解析完成以後,瀏覽器引擎會經過dom tree和css rule tree來構建rendering tree:api
rendering tree和dom tree並不徹底相同,例如:<head></head>或display:none的東西就不會放在渲染樹中。瀏覽器
css rule tree主要是完成匹配,並把css rule附加給rendering tree的每一個element。緩存
在渲染樹構建完成後,服務器
瀏覽器會對這些元素進行定位和佈局,這一步也叫作reflow或者layout。
瀏覽器繪製這些元素的樣式,顏色,背景,大小及邊框等,這一步也叫作repaint。
而後瀏覽器會將各層的信息發送給GPU,GPU會將各層合成;顯示在屏幕上。
如上所說,渲染樹構建完成後;瀏覽器要作的步驟:
reflow——》repaint——》composite
reflow和repaint都是耗費瀏覽器性能的操做,這二者尤以reflow爲甚;由於每次reflow,瀏覽器都要從新計算每一個元素的形狀和位置。
因爲reflow和repaint都是很是消耗性能的,咱們的瀏覽器爲此作了一些優化。瀏覽器會將reflow和repaint的操做積攢一批,而後作一次reflow。可是有些時候,你的代碼會強制瀏覽器作屢次reflow。例如:
var content = document.getElementById('content'); content.style.width = 700px; var contentWidth = content.offsetWidth; content.style.backgound = 'red';
以上第三行代碼,須要瀏覽器reflow後;再獲取值,因此會致使瀏覽器多作一次reflow。
下面是一些針對reflow和repaint的最佳實踐:
不要一條一條地修改dom的樣式,儘可能使用className一次修改。
將dom離線後修改
使用documentFragment對象在內存裏操做dom。
先把dom節點display:none;(會觸發一次reflow)。而後作大量的修改後,再把它顯示出來。
clone一個dom節點在內存裏,修改以後;與在線的節點相替換。
不要使用table佈局,一個小改動會形成整個table的從新佈局。
transform和opacity只會引發合成,不會引發佈局和重繪。
從上述的最佳實踐中你可能發現,動畫優化通常都是儘量地減小reflow、repaint的發生。關於哪些屬性會引發reflow、repaint及composite,你能夠在這個網站找到https://csstriggers.com/。
在reflow和repaint以後,瀏覽器會將多個複合層傳入GPU;進行合成工做,那麼合成是如何工做的呢?
假設咱們的頁面中有A和B兩個元素,它們有absolute和z-index屬性;瀏覽器會重繪它們,而後將圖像發送給GPU;而後GPU將會把多個圖像合成展現在屏幕上。
<style> #a, #b { position: absolute; } #a { left: 30px; top: 30px; z-index: 2; } #b { z-index: 1; } </style> <div id="#a">A</div> <div id="#b">B</div>
咱們將A元素使用left屬性,作一個移動動畫:
<style> #a, #b { position: absolute; } #a { left: 10px; top: 10px; z-index: 2; animation: move 1s linear; } #b { left: 50px; top: 50px; z-index: 1; } @keyframes move { from { left: 30px; } to { left: 100px; } } </style> <div id="#a">A</div> <div id="#b">B</div>
在這個例子中,對於動畫的每一幀;瀏覽器會計算元素的幾何形狀,渲染新狀態的圖像;並把它們發送給GPU。(你沒看錯,position也會引發瀏覽器重排的)儘管瀏覽器作了優化,在repaint時,只會repaint部分區域;可是咱們的動畫仍然不夠流暢。
由於重排和重繪發生在動畫的每一幀,一個有效避免reflow和repaint的方式是咱們僅僅畫兩個圖像;一個是a元素,一個是b元素及整個頁面;咱們將這兩張圖片發送給GPU,而後動畫發生的時候;只作兩張圖片相對對方的平移。也就是說,僅僅合成緩存的圖片將會很快;這也是GPU的優點——它能很是快地以亞像素精度地合成圖片,並給動畫帶來平滑的曲線。
爲了僅發生composite,咱們作動畫的css property必須知足如下三個條件:
不影響文檔流。
不依賴文檔流。
不會形成重繪。
知足以上以上條件的css property只有transform和opacity。你可能覺得position也知足以上條件,但事實不是這樣,舉個例子left屬性可使用百分比的值,依賴於它的offset parent。還有em、vh等其餘單位也依賴於他們的環境。
咱們使用translate來代替left
<style> #a, #b { position: absolute; } #a { left: 10px; top: 10px; z-index: 2; animation: move 1s linear; } #b { left: 50px; top: 50px; z-index: 1; } @keyframes move { from { transform: translateX(0); } to { transform: translateX(70px); } } </style> <div id="#a">A</div> <div id="#b">B</div>
瀏覽器在動畫執行以前就知道動畫如何開始和結束,由於瀏覽器沒有看到須要reflow和repaint的操做;瀏覽器就會畫兩張圖像做爲複合層,並將它們傳入GPU。
這樣作有兩個優點:
動畫將會很是流暢
動畫不在綁定到CPU,即便js執行大量的工做;動畫依然流暢。
看起來性能問題好像已經解決了?在下文你會看到GPU動畫的一些問題。
GPU實際上能夠看做一個獨立的計算機,它有本身的處理器和存儲器及數據處理模型。當瀏覽器向GPU發送消息的時候,就像向一個外部設備發送消息。
你能夠把瀏覽器向GPU發送數據的過程,與使用ajax向服務器發送消息很是相似。想一下,你用ajax向服務器發送數據,服務器是不會直接接受瀏覽器的存儲的信息的。你須要收集頁面上的數據,把它們放進一個載體裏面(例如JSON),而後發送數據到遠程服務器。
一樣的,瀏覽器向GPU發送數據也須要先建立一個載體;只不過GPU距離CPU很近,不會像遠程服務器那樣可能幾千裏那麼遠。可是對於遠程服務器,2秒的延遲是能夠接受的;可是對於GPU,幾毫秒的延遲都會形成動畫的卡頓。
瀏覽器向GPU發送的數據載體是什麼樣?這裏給出一個簡單的製做載體,並把它們發送到GPU的過程。
畫每一個複合層的圖像
準備圖層的數據
準備動畫的着色器(若是須要)
向GPU發送數據
因此你能夠看到,每次當你添加transform:translateZ(0)
或will-change:transform
給一個元素,你都會作一樣的工做。重繪是很是消耗性能的,在這裏它尤爲緩慢。在大多數狀況,瀏覽器不能增量重繪。它不得不重繪先前被複合層覆蓋的區域。
還記得剛纔a元素和b元素動畫的例子嗎?如今咱們將b元素作動畫,a元素靜止不動。
和剛纔的例子不一樣,如今b元素將擁有一個獨立複合層;而後它們將被GPU合成。可是由於a元素要在b元素的上面(由於a元素的z-index比b元素高),那麼瀏覽器會作什麼?瀏覽器會將a元素也單獨作一個複合層!
因此咱們如今有三個複合層a元素所在的複合層、b元素所在的複合層、其餘內容及背景層。
一個或多個沒有本身複合層的元素要出如今有複合層元素的上方,它就會擁有本身的複合層;這種狀況被稱爲隱式合成。
瀏覽器將a元素提高爲一個複合層有不少種緣由,下面列舉了一些:
3d或透視變換css屬性,例如translate3d,translateZ等等(js通常經過這種方式,使元素得到複合層)
<video><iframe><canvas><webgl>等元素。
混合插件(如flash)。
元素自身的 opacity和transform 作 CSS 動畫。
擁有css過濾器的元素。
使用will-change屬性。
position:fixed。
元素有一個 z-index 較低且包含一個複合層的兄弟元素(換句話說就是該元素在複合層上面渲染)
這看起來css動畫的性能瓶頸是在重繪上,可是真實的問題是在內存上:
使用GPU動畫須要發送多張渲染層的圖像給GPU,GPU也須要緩存它們以便於後續動畫的使用。
一個渲染層,須要多少內存佔用?爲了便於理解,舉一個簡單的例子;一個寬、高都是300px的純色圖像須要多少內存?
300 300 4 = 360000字節,即360kb。這裏乘以4是由於,每一個像素須要四個字節計算機內存來描述。
假設咱們作一個輪播圖組件,輪播圖有10張圖片;爲了實現圖片間平滑過渡的交互;爲每一個圖像添加了will-change:transform。這將提高圖像爲複合層,它將多須要19mb的空間。800 600 4 * 10 = 1920000。
僅僅是一個輪播圖組件就須要19m的額外空間!
在chrome的開發者工具中打開setting——》Experiments——》layers能夠看到每一個層的內存佔用。如圖所示:
如今咱們能夠總結一下GPU動畫的優勢和缺點:
每秒60幀,動畫平滑、流暢。
一個合適的動畫工做在一個單獨的線程,它不會被大量的js計算阻塞。
3D「變換」是便宜的。
缺點:
提高一個元素到複合層須要額外的重繪,有時這是慢的。(即咱們獲得的是一個全層重繪,而不是一個增量)
繪圖層必須傳輸到GPU。取決於層的數量和傳輸可能會很是緩慢。這可能讓一個元素在中低檔設備上閃爍。
每一個複合層都須要消耗額外的內存,過多的內存可能致使瀏覽器的崩潰。
若是你不考慮隱式合成,而使用重繪;會致使額外的內存佔用,而且瀏覽器崩潰的機率是很是高的。
咱們會有視覺假象,例如在Safari中的文本渲染,在某些狀況下頁面內容將消失或變形。
保持動畫的對象的z-index儘量的高。理想的,這些元素應該是body元素的直接子元素。固然,這不是總可能的。因此你能夠克隆一個元素,把它放在body元素下僅僅是爲了作動畫。
將元素上設置will-change CSS屬性,元素上有了這個屬性,瀏覽器會提高這個元素成爲一個複合層(不是老是)。這樣動畫就能夠平滑的開始和結束。可是不要濫用這個屬性,不然會大大增長內存消耗。
如上所說,transform和opacity保證了元素屬性的變化不影響文檔流、也不受文檔流影響;而且不會形成repaint。
有些時候你可能想要改變其餘的css屬性,做爲動畫。例如:你可能想使用background屬性改變背景:
<div class="bg-change"></div> .bg-change { width: 100px; height: 100px; background: red; transition: opacity 2s; } .bg-change:hover { background: blue; }
在這個例子中,在動畫的每一步;瀏覽器都會進行一次重繪。咱們可使用一個復層在這個元素上面,而且僅僅變換opacity屬性:
<div class="bg-change"></div> <style> .bg-change { width: 100px; height: 100px; background: red; } .bg-change::before { content: ''; display: block; width: 100%; height: 100%; background: blue; opacity: 0; transition: opacity 20s; } .bg-change:hover::before { opacity: 1; } </style>
看一下兩張圖片,有什麼不一樣嗎?
這兩張圖片視覺上是同樣的,可是它們的尺寸一個是39kb;另一個是400b。不一樣之處在於,第二個純色層是經過scale放大10倍作到的。
<div id="a"></div> <div id="b"></div> <style> #a, #b { will-change: transform; } #a { width: 100px; height: 100px; } #b { width: 10px; height: 10px; transform: scale(10); } </style>
對於圖片,你要怎麼作呢?你能夠將圖片的尺寸減小5%——10%,而後使用scale將它們放大;用戶不會看到什麼區別,可是你能夠減小大量的存儲空間。
css動畫有一個重要的特性,它是徹底工做在GPU上。由於你聲明瞭一個動畫如何開始和如何結束,瀏覽器會在動畫開始前準備好全部須要的指令;並把它們發送給GPU。而若是使用js動畫,瀏覽器必須計算每一幀的狀態;爲了保證平滑的動畫,咱們必須在瀏覽器主線程計算新狀態;把它們發送給GPU至少60次每秒。除了計算和發送數據比css動畫要慢,主線程的負載也會影響動畫; 當主線程的計算任務過多時,會形成動畫的延遲、卡頓。
因此儘量地使用基於css的動畫,不只僅更快;也不會被大量的js計算所阻塞。
減小瀏覽器的重排和重繪的發生。
不要使用table佈局。
css動畫中儘可能只使用transform和opacity,這不會發生重排和重繪。
儘量地只使用css作動畫。
避免瀏覽器的隱式合成。
改變複合層的尺寸。
GPU合成主要參考:
https://www.smashingmagazine....
哪些屬性會引發reflow、repaint及composite,你能夠在這個網站找到: