CSS Animation性能優化
CSS Animation是實現Web Animation方法之一,其主要通過 @keyframes 和 animation-* 或者 transition 來實現一些Web動效。不過今天我們聊的不是怎么制作Web動畫,咱們來聊聊CSS Animation性能相關的話題。
瀏覽器渲染原理
本文章詳細闡述了瀏覽器工作原理,下面用兩張圖來分別描述Firefox和Chrome瀏覽器對Web頁面的渲染過程:
Chrome渲染過程
Firefox渲染過程
特別聲明:接下來的內容都是針對于Chrome瀏覽器進行討論。
Chrome渲染部分的實際含義
從上面的流程圖中不難看出,Chrome渲染主要包括Parse Html、Recalculate Style、Layout、Rasterizer、Paint、Image Decode、Image Resize和Composite Layers等。簡單了解一下其含義,以便后續內容的更好理解。
Parse Html
發送一個 http 請求,獲取請求的內容,然后解析HTML的過程。
有一個經典的前端面試題: 當你在瀏覽器中輸入google.com并且按下回車之后發生了什么? 這個面試題或許能幫助大家更好的理解Parse Html,甚至是瀏覽器渲染的其他幾個部分。
Recalculate Style
重新計算樣式,它計算的是Style,和Layout做的事情完全不同。Layout計算的是一個元素絕對的位置和尺寸,或者說是“Compute Layout”。
Recalculate被觸發的時候做的事情就是處理JavaScript給元素設置的樣式而已。Recalculate Style會計算Render樹(渲染樹),然后從根節點開始進行頁面渲染,將CSS附加到DOM上的過程。
任何企圖改變元素樣式的操作都會觸發Recalculate。同Layout一樣,它也是在JavaScript執行完成后才觸發的。
Layout
計算頁面上的布局,即元素在文檔中的位置及大小。正如前面所述,Layout計算的是布局位置信息。任何有可能改變元素位置或大小的樣式都會觸發這個Layout事件。
觸發Layout的屬性非常的多,如果想了解什么屬性會觸發Layout事件,可以在 CSS Triggers 網站查閱。下圖截了一部分:
Rasterizer
光柵化,一般的安卓手機都會進行光柵化,光柵主要是針對圖形的一個柵格化過程。低端手機在這部分耗時還是蠻多的。
Paint
頁面上顯示東西有任何變動都會觸發Paint。包括拖動滾動條,鼠標選擇中文字等這些完全不改變樣式,只改變顯示結果的動作都會觸發Paint。
Paint的工作就是把文檔中用戶可見的那一部分展現給用戶。Paint是把Layout和Recalculate的計算的結果直接在瀏覽器視窗上繪制出來,它并不實現具體的元素計算。
Image Decode
圖片解碼,將圖片解析到瀏覽器上顯示的過程。
Image Resize
圖片的大小設置,圖片加載解析后,若發現圖片大小并不是實際的大小(CSS改變了寬度),則需要Resize。Resize越大,耗時越久,所以盡量以圖片的原始大小輸出。
Composite Layers
最后合并圖層,輸出頁面到屏幕。瀏覽器在渲染過程中會將一些含有特殊樣式的DOM結構繪制于其他圖層,有點類似于PhotoShop的圖層概念。一張圖片在PotoShop是由多個圖層組合而成,而瀏覽器最終顯示的頁面實際也是有多個圖層構成的。
下面這些因素都會導致新圖層的創建:
-
進行3D或者透視變換的CSS屬性
-
使用硬件加速視頻解碼的 <video> 元素
-
具有3D(WebGL)上下文或者硬件加速的2D上下文的 <canvas> 元素
-
組合型插件(即Flash)
-
具有有CSS透明度動畫或者使用動畫式Webkit變換的元素
-
具有硬件加速的CSS濾鏡的元素
像素渲染流水線
通過前面的介紹,在屏幕上最終呈現的頁面,是類似于圖層一樣合并輸出到屏幕上的。其實所寫的Web頁面最終以像素的形式在瀏覽器屏幕上呈現。這樣一來,我們需要理解所寫的頁面代碼是如何被轉換成屏幕上顯示的像素。這個轉換過程可以歸納為這樣的一個流水線,主要包含五個關鍵步驟:
-
JavaScript :一般來說,我們會使用JavaScript來實現一些視覺變化的效果。比如CSS Animation、Transition和Web Animation API。
-
Style :計算樣式。這個過程是根據CSS選擇器,對每個DOM元素匹配對應的CSS樣式。這一步結束之后,就確定了每個DOM元素上應該應用什么CSS樣式規則。
-
Layout :布局。上一步確定了每個DOM元素的樣式規則,這一步就是具體計算每個DOM元素最終在屏幕上顯示的大小和位置。Web頁面中元素的布局是相對的,因此一個元素的布局發生變化,會聯動地引發其他元素的布局發生變化。比如, <body> 元素的 width 變化會影響其后代元素的寬度。因此,對于瀏覽器而言, 布局過程是經常發生的 。
-
Paint :繪制。本質上就是填充像素的過程。包括繪制文字、顏色、圖像、邊框和陰影等,也就是一個DOM元素所有的可視效果。一般來說,這個繪制過程是在多個層上完成的。
-
Composite :渲染層合并。前面也說過,對于頁面中DOM元素的繪制是在多個層上進行的。在每個層上完成繪制過程之后,瀏覽器會將所有層按照合理的順序合并成一個圖層,然后在屏幕上呈現。對于有位置重疊的元素的頁面,這個過程尤其重要,因為一量圖層的合并順序出錯,將會導致元素顯示異常。
上述過程的每一步都有可能會發生,因此一定要弄清楚自己的代碼將會運行在哪一步。
雖然在理論上,而面的每一幀都是結過上述的流水線處理之后渲染出來的,但并不意味著頁面每一幀的渲染都需要經過上述五個步驟的處理。實際上,對視覺變化效果的一個幀的渲染,有三種常用的流水線。
JavaScript/CSS =>計算樣式=>布局=>繪制=>渲染層合并
如果你修改一個DOM元素的“Layout”屬性,也就是改變了元素的樣式(比如 width 、 height 或者 position 等),那么瀏覽器會檢查哪些元素需要重新布局,然后對頁面激發一個reflow(重排)過程完成重新布局。被reflow(重排)的元素,接下來也會激發繪制過程,最后激發渲染層合并過程,生成最后的畫面。
reflow又叫重排,是指瀏覽器計算頁面的全部或部分布局所做的處理。reflow必定會引發重繪,這對于Web的性能影響是極大的。
JavaScript/CSS => 計算樣式 =>繪制 =>渲染層合并
如果你修改一個DOM元素的“Paint Only”屬性,比如背景圖片、文字顏色或陰影等,這些屬性不會影響頁面的布局,因此瀏覽器會在完成樣式計算之后,跳過布局過程,只會繪制和渲染層合并過程。
JavaScript/CSS => 計算樣式 =>渲染層合并
如果你修改一個非樣式且非繪制的CSS屬性,那么瀏覽器會在完成樣式計算之后,跳過布局和繪制的過程,直接做渲染層合并。這種方式在性能上是最理想的,對于動畫和滾動這種負荷很重的渲染,我們要爭取使用第三種渲染過程。
通過前面這么多的內容介紹,我們可以得知,影響Web性能主要過程包括Layout、Paint和Composite。那么對于CSS Animation而言,我們的所有操作都是通過CSS的樣式控制動畫,言外之意,只要是會觸發Layout、Paint和Composite的CSS屬性都會直接影響動畫的性能。在CSS中所有影響Layout、Paint和Composite的屬性都可以通過 。那么如何避免達到前面所述的,整個動畫盡量避開重排和重繪,只做渲染層合并呢?暫且先不討論,把這部分放到最后面來討論。接下來接著先看看其他相關的知識點。
渲染性能
在理解渲染性能之前,我們有必要先了解前面提到的兩個概念 重排(也就是回流) 和 重繪 。因為這兩者與前面介紹的像素渲染流水線中的 Layout 和 Paint 都有關系,而且Layout和Paint對性能的渲染又有莫大的關系。
Reflow(重排)
Reflow(重排)指的是計算頁面布局(Layout)。某個節點Reflow時會重新計算節點的尺寸和位置,而且還有可能觸其后代節點Reflow。在這之后再次觸發一次Repaint(重繪)
當Render Tree中的一部分(或全部)因為元素的尺寸、布局、隱藏等改變而需要重新構建。這就稱為回流,每個頁面至少需要一次回流,就是頁面第一次加載的時候。
在Web頁面中,很多狀況下會導致回流:
- 調整窗口大小
- 改變字體
- 增加或者移除樣式表
- 內容變化
- 激活CSS偽類
- 操作CSS屬性
- JavaScript操作DOM
- 計算 offsetWidth 和 offsetHeight
- 設置 style 屬性的值
- CSS3 Animation或Transition
Repaint(重繪)
Repaint(重繪)或者Redraw遍歷所有節點,檢測節點的可見性、顏色、輪廓等可見的樣式屬性,然后根據檢測的結果更新頁面的響應部分。
當Render Tree中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀、風格、而不會影響布局的。就是重繪。
將重排和重繪的介紹結合起來,不難發現: 重繪(Repaint)不一定會引起回流(Reflow重排),但回流必將引起重繪(Repaint) 。
既然如此,那么什么情況之下會觸發瀏覽器的Repaint和Reflow呢?
-
頁面首次加載
-
DOM元素添加、修改(內容)和刪除(Reflow + Repaint)
-
僅修改DOM元素的顏色(只有Repaint,因為不需要調整布局)
-
應用新的樣式或修改任何影響元素外觀的屬性
-
Resize瀏覽器窗口和滾動頁面
-
讀取元素的某些屬性( offsetLeft 、 offsetTop 、 offsetHeight 、 offsetWidth 、 getComputedStyle() 等)
可以說Reflow和Repaint都很容易觸發,而它們的觸發對性能的影響都非常大,但非常不幸的是,我們無法完全避免,只能盡量不去觸發瀏覽器的Reflow和Repaint。
從前面的內容可以了解到,Reflow和Repaint對性能影響很大,那么具體哪些點會影響到渲染性能呢?
影響Layout的屬性
當你改變頁面上某個元素的時候,瀏覽器需要做一次重新布局的操作,這次操作會包括計算受操作影響所有元素的幾何數,比如每個元素的位置和尺寸。如果你修改了 html 這個元素的 width 屬性,那么整個頁面都會被重繪。
由于元素相覆蓋,相互影響,稍有不慎的操作就有可能導致一次自上而下的布局計算。所以我們在進行元素操作的時候要一再小心盡量避免修改這些重新布局的屬性。
影響Repaint的屬性
有些屬性的修改不會觸發重排,但會觸Repaint(重繪),現代瀏覽器中主要的繪制工作主要用光柵化軟件來完成。所以重新會制的元素是否會很大程度影響你的性能,是由這個元素和繪制層級的關系來決定的,如果這個元素蓋住的元素都被重新繪制,那么代價自然就相當地大。
如果你在動畫里面使用了上述某些屬性,導致重繪,這個元素所屬的圖層會被重新上傳到GPU。在移動設備上這是一個很昂貴耗資源的操作,因為移動設備的CPU明顯不如你的電腦,這也意味著繪制的工作會需要更長的時間;而上傳線CPU和GPU的帶寬并非沒有限制,所以重繪的紋理上傳就自然需要更長的時間。
CSS Triggers 網站中可以得知哪些屬性會觸發重排、哪些屬性會觸發重繪以及哪些屬性會觸合成。但并不是CSS中所有的屬性都可以用于CSS Animation和Transition中的。在W3C官方規范中明確定了哪些CSS屬性可以用于 Animation 和 Transition 中。 @Rodney Rehm 還對這些 屬性做過一個兼容測試 。如果你想深入的了解這方面的知識,建議您閱讀下面兩篇文章:
如此一來,我們知道可用于CSS Animation或者Transition的CSS屬性之后,再配合 CSS Triggers 網站,可以輕易掌握哪些CSS屬性會觸發重排、重繪和合成等。 雖然無法避免,但我們可以盡量控制 。
性能優化
如果我們知道瀏覽器是如何渲染一個頁面的,并且去優化渲染過程中的關鍵步驟,不是是就能事半功倍呢?
有關于這部分的介紹,建議大家閱讀《 渲染性能 》。
在像素渲染流水線中,得知,如果我們能幸運的避免Layout和Paint,那么性能是最好的,言外之意,動畫性能也將變得最佳。那么在CSS中可能通過不同的方式來創建新圖層。其實這也就是大家常說的,通過CSS的屬性來觸發GPU加速。瀏覽器會為此元素單獨創建一個“層”。當有單獨的層之后,此元素的Repaint操作將只需要更新自己,不用影響到別人。你可以將其理解為局部更新。所以開啟了硬件加速的動畫會變得流暢很多。
為什么開啟硬件加速動畫就會變得流暢,那是因為每個頁面元素都有一個獨立的Render進程。Render進程中包含了主線程和合成線程,主線程負責:
-
JavaScript的執行
-
CSS樣式計算
-
計算Layout
-
將頁面元素繪制成位圖(Paint)
-
發送位圖給合成線程
合成線程則主要負責:
-
將位圖發送給GPU
-
計算頁面的可見部分和即將可見部分(滾動)
-
通知GPU繪制位圖到屏幕上(Draw)
我們可以得到一個大概的瀏覽器線程模型:
我們可以將頁面繪制的過程分為三個部分:Layout、Paint和合成。Layout負責計算DOM元素的布局關系,Paint負責將DOM元素繪制成位圖,合成則負責將位圖發送給GPU繪制到屏幕上(如果有 transform 、 opacity 等屬性則通知GPU做處理)。
GPU加速其實是一直存在的,而如同 translate3D 這種hack只是為了讓這個元素生成獨立的 GraphicsLayer , 占用一部分內存,但同時也會在動畫或者Repaint的時候不會影響到其他任何元素,對高刷新頻率的東西,就應該分離出單獨的一個 GraphicsLayer。
GPU對于動畫圖形的渲染處理比CPU要快。
RenderLayer 樹,滿足以下任意一點的就會生成獨立一個 RenderLayer。
- 頁面的根節點的RenderObject
- 有明確的CSS定位屬性( relative , absolute 或者 transform )
- 是透明的
- 有CSS overflow、CSS alpha遮罩(alpha mash)或者CSS reflection
- 有CSS 濾鏡(fliter)
- 3D環境或者2D加速環境的canvas元素對應的RenderObject
- video元素對應的RenderObject
每個RenderLayer 有多個 GraphicsLayer 存在
- 有3D或者perspective transform的CSS屬性的層
- 使用加速視頻解碼的video元素的層
- 3D或者加速2D環境下的canvas元素的層
- 插件,比如flash(Layer is used for a composited plugin)
- 對 opacity 和 transform 應用了CSS動畫的層
- 使用了加速CSS濾鏡(filters)的層
- 有合成層后代的層
- 同合成層重疊,且在該合成層上面(z-index)渲染的層
每個GraphicsLayer 生成一個 GraphicsContext, 就是一個位圖,傳送給GPU,由GPU合成放出。
那么就是說,GraphicsLayer過少則每次repaint大整體的工作量巨大,而過多則repaint小碎塊的次數過多。這種次數過多就稱為 層數爆炸 ,為了防止這個爆炸 Blink 引擎做了一個特殊處理。
扯了這么多,我們可以稍微總結一下下:
不是所有屬性動畫消耗的性能都一樣,其中消耗最低的是 transform 和 opacity 兩個屬性(當然還有會觸發Composite的其他CSS屬性),其次是Paint相關屬性。所以在制作動畫時,建議 使用 transform 的 translate 替代 margin 或 position 中的 top 、 right 、 bottom 和 left ,同時使用 transform 中的 scaleX 或者 scaleY 來替代 width 和 height 。
為了確保頁面的流程,必須保證 60fps 內不發生兩次渲染樹更新,比如下圖, 16ms 內只發生如下幾個操作則是正常及正確的:
頁面滾動時,需要避免不必要的渲染及長時間渲染。其中不必要的渲染包括:
-
position:fixed; 。 fixed 定位在滾動時會不停的進行渲染,特別是頁面頂部有一個 fixed ,頁面底部有個類似返回頂部的 fixed ,則在滾動時會對整個頁面進行渲染,效率非常低。可以通過 transform: translateZ(0) 或者 transform: translate3d(0,0,0) 來解決
-
overflow:scroll 。前面說了,而在滾動也會觸發Repaint和Reflow。在調試過程中注意到一個有趣的現象,有時打開了頁面并不會導致crash,但快速滑動的時候卻會。由于crash是頁面本身內存占比過高,只要優化了頁面的內存占用,滑動自然也不會是很大的問題。無論你在什么時候滑動頁面,頁面滾動都是一個不斷重新組合重新繪制的過程。所以 減少渲染區域在滾動里就顯得非常重要 。
-
CSS偽類觸發。有些CSS偽類在頁面滾動時會不小心觸發到。比如 :hover 效果有 box-shadow 、 border-radius 等比較耗時的CSS屬性時,建議頁面滾動時,先取消 :hover 效果,滾動停止后再加上 :hover 效果。這個可以通過在外層添加類名進行控制。但添加類名、刪除類名也會改變元素時,瀏覽器就會要重新做一次計算和布局。所以千萬要小心這種無意觸發重新布局的操作,有的時候可能不是動畫,但去付出的代價要比做一個動畫更加昂貴。也就是說 classname 變化了,就一定會出現一次rendering計算,如果一定需要這么做,那可以使用 classlist 的方法。
-
touch 事件的監聽
長時間渲染包括:
-
復雜的CSS
-
Image Decodes:特別是圖片的Image Decodes及Image Resize這兩個過程在移動端是非常耗時的
-
Large Empty Layers: 大的空圖層
在CSS中除了開啟3D加速能明顯的讓動畫變得流暢之外,在CSS中提供了一個新的CSS特性: will-change 。其主要作用就是 提前告訴瀏覽器我這里將會進行一些變動,請分配資源(告訴瀏覽器要分配資源給我) 。
will-change 屬性,允許作者提前告知瀏覽器的默認樣式,那他們可能會做出一個元素。它允許對瀏覽器默認樣式的優化如何提前處理因素,在動畫實際開始之前,為準備動畫執行潛在昂貴的工作。有關于 will-change 更詳細的介紹可以點擊這里。
話說回來, will-change 并不是萬能的,不是說使用了 will-change 就對動畫的性能有提高,而是要正確使用,才會有所改為。在使用 will-change 時應該注意:
-
不要將 will-change 應用到太多元素上:瀏覽器已經盡力嘗試去優化一切可以優化的東西了。有一些更強力的優化,如果與 will-change 結合在一起的話,有可能會消耗很多機器資源,如果過度使用的話,可能導致頁面響應緩慢或者消耗非常多的資源。
-
有節制地使用:通常,當元素恢復到初始狀態時,瀏覽器會丟棄掉之前做的優化工作。但是如果直接在樣式表中顯式聲明了 will-change 屬性,則表示目標元素可能會經常變化,瀏覽器會將優化工作保存得比之前更久。所以最佳實踐是當元素變化之前和之后通過腳本來切換 will-change 的值。
-
不要過早應用 will-change 優化:如果你的頁面在性能方面沒什么問題,則不要添加 will-change 屬性來榨取一丁點的速度。 will-change 的設計初衷是作為最后的優化手段,用來嘗試解決現有的性能問題。它不應該被用來預防性能問題。過度使用 will-change 會導致大量的內存占用,并會導致更復雜的渲染過程,因為瀏覽器會試圖準備可能存在的變化過程。這會導致更嚴重的性能問題。
-
給它足夠的工作時間:這個屬性是用來讓頁面開發者告知瀏覽器哪些屬性可能會變化的。然后瀏覽器可以選擇在變化發生前提前去做一些優化工作。所以給瀏覽器一點時間去真正做這些優化工作是非常重要的。使用時需要嘗試去找到一些方法提前一定時間獲知元素可能發生的變化,然后為它加上 will-change 屬性。
在使用 will-change 一定要注意方式方法,比如常見的錯誤方法是直接在 :hover 是使用,并沒有告訴瀏覽器分配資源:
.element:hover {
will-change: transform;
transition: transform 2s;
transform: rotate(30deg) scale(1.5);
}
其正確使用的方法是,在進入父元素的時候就告訴瀏覽器,你該分配一定的資源:
.element {
transition: opacity .3s linear;
}
/* declare changes on the element when the mouse enters / hovers its ancestor */
.ancestor:hover .element {
will-change: opacity;
}
/* apply change when element is hovered */
.element:hover {
opacity: .5;
}
另外在應用變化之后,取消 will-change 的資源分配:
var el = document.getElementById('demo');
el.addEventListener('animationEnd', removeHint);
function removeHint() {
this.style.willChange = 'auto';
}
除了 will-change 能讓我們在制作動畫變得更為流暢之外,在CSS層面上,還有別的方案嗎?這個答案是肯定的。前面通過大幅的篇幅了解到,影響性能主要是因為重繪和重排。針對于這方面,CSS提供了一個新的屬性 contain 。
總結
本文主要介紹了瀏覽器渲染一個Web頁面的原理,從中了解到影響Web的性能因素,從底層找到影響Web渲染性能的主要因素是CSS的屬性會觸發瀏覽器的重繪、重排。而CSS的動畫,也主要是控制CSS的屬性,從這一方面說明,影響動畫的性能也是造成重繪和重排的CSS。為了讓一個動畫能更佳的流暢,我們就要從技術的手段避免CSS的屬性造成瀏覽器的重繪和重排。以及利用一些CSS的新屬性,讓動畫的性能更好,也就是讓動畫更為流暢。
文章整理的感覺有些零亂,上述內容如果有不對之處,還希望大嬸們多多指點。
參考資料
- 無線性能優化:Composite
- webkit瀏覽器渲染影響因素分析
- 搞定這些疑難雜癥,向CSS3動畫說yes
- 提高頁面的渲染性能
- 回流與重繪:CSS性能讓JavaScript變慢?
- 頁面重繪和回流以及優化
- repaint and reflow (重繪和回流)
- What forces layout / reflow
- 10 Ways to Minimize Reflows and Improve Performance
來自:http://www.w3cplus.com/animation/animation-performance.html