優化動畫卡頓:卡頓原因分析及優化方案

熱愛改名阿呆呆發表於2019-03-14

目錄

一、動畫卡頓分析

動畫卡頓的原因

大多數裝置的重新整理頻率是60次/秒,也就是1秒鐘的動畫是由60個畫面連在一起生成的,所以要求瀏覽器對每一幀畫面的渲染工作要在16ms內完成。當渲染時間超出16ms時,1秒鐘內少於60個畫面生成,就會有不連貫、卡頓的感覺,影響使用者體驗。

頁面渲染流程

一個頁面幀在客戶端的渲染分為以下幾步:

頁面渲染流程 來源:Google

  1. JavaScript:JavaScript實現動畫效果,DOM操作等。
  2. Style(樣式計算):確認每個DOM元素應用的CSS樣式規則。
  3. Layout(佈局):計算每個DOM元素最終在螢幕上的大小和位置。由於DOM元素的佈局是相對的,所以當某個元素髮生變化影響了佈局時,其他元素也會隨之變化,則需要回退重新渲染,這個過程稱之為reflow。
  4. Paint(繪製):在多個層上繪製DOM元素的文字、顏色、影象、邊框和陰影等。
  5. Composite(Render Layer合併):按照合理的順序合併圖層並顯示到螢幕上。 瀏覽器在實際渲染頁面的時候需要經過一系列的對映,由HTML頁面構建出來的DOM樹到最終的圖層,對映過程如下圖(來源:參考[3])所示(注意下圖類名在後續有所更改,RenderObject->LayoutObject,RenderLayer->PaintLayer):
    The Compositing Tree
  • Node->RenderObject:DOM樹的每個Node都有一個對應的RenderObject(一對一關係,RenderObject包含了Node的內容);

  • RenderObject -> RenderLayer:一個或多個RenderObject對應一個RenderLayer(多對一),RenderLayer用於保證元素之間的層級關係,一般來說位於同一位置的且層級相同的元素位於同一個Render Layer,只有某些特殊的RenderObject會專門建立一個新的渲染層,其他的RenderObject與第一個擁有RenderLayer的祖先元素共用一個。常見的生成RenderLayer的RenderObject擁有以下的一種特徵參考[3]

    • 頁面根元素
    • 有CSS定位屬性(relative, absolute, fixed, sticky)
    • transparent不為1
    • overflow不為visible
    • 有CSS mask屬性
    • 有CSS box-reflect屬性
    • 有CSS filter屬性
    • 3D或硬體加速的2D canvas元素
    • video元素
  • RenderLayer -> GraphicsLayer:一個或多個RenderLayer對應一個GraphicsLayer(多對一),某些被認為是Compositing Layer的RenderLayer單獨對應一個GraphicsLayer,其他RenderLayer與第一個擁有GraphicsLayer的祖先元素共用一個GraphicsLayer。每個GraphicsLayer有一個GraphicsContext用於繪製其對應的RenderLayers,合成器將GraphicsContexts的點陣圖合成,最終顯示到螢幕上。渲染層提升為合成層的原因如下:

    • 有3D transform屬性
    • 有perspective屬性
    • 3D canvas或硬體加速的2D canvas
    • 硬體加速的iframe元素(如iframe嵌入的頁面有合成層,合成層需要硬體加速)
    • 使用了硬體加速的外掛,如flash
    • 對opacity/transform屬性應用了animation/transition(當animation/transition為active)
    • 子元素是compositing layer
    • 兄弟元素是compositing layer,與當前的非composting layer有重疊,層級低於當前層
    • 有will-change屬性

二、優化方法

在網上可以看到很多的優化方案總結,大佬們都寫的很好。

Talk is cheap. Show me the code.

結合頁面渲染流程,這裡將結合一些測試程式碼,分析動畫的各種優化方案和效果:

  • JavaScript:優化JavaScript的執行效率
    • requestAnimationFrame代替setTimeoutsetInterval
    • 可並行的DOM元素更新劃分為多個小任務
    • DOM無關的耗時操作放到Web Workers
  • Style:降低樣式計算複雜度和範圍
    • 降低樣式選擇器的複雜度
    • 減少需要執行樣式計算的元素個數
  • Layout:避免大規模、複雜的佈局
    • 避免頻繁改變佈局
    • 用flexbox佈局替代老的佈局模型
    • 避免強制同步佈局事件
  • Paint/Composite:GPU加速
    • 將移動或漸變元素由渲染層(RenderLayer)提升為合成層(Compositing Layer)
    • 避擴音升合成層的陷阱

JavaScript:優化JavaScript的執行效率

1. requestAnimationFrame代替setTimeoutsetInterval

為什麼setTimeoutsetInterval不好?
由於js是單執行緒執行,所以為了防止某個任務執行時間過長而導致程式阻塞,js中存在非同步佇列的概念,對於如setTimeoutajax請求都是把程式放到了非同步佇列中,當主程式為空時才執行非同步佇列中的任務。所以 setTimeoutsetInterval無法保證回撥函式的執行時機,可能會在一幀之內執行多次導致多次頁面渲染,浪費CPU資源甚至產生卡頓,或者是在一幀即將結束時執行導致重新渲染,出現掉幀的情況。
requestAnimationFrame是怎麼優化的?

  • CPU節能,當頁面被隱藏或最小化時,暫停渲染。
  • 函式節流,其迴圈間隔是由螢幕重新整理頻率決定的,保證回撥函式在螢幕的每一次重新整理間隔中只執行一次。

優化效果具體如何?DEMO
通過chrome的performance皮膚檢視具體表現的差別。
通過setTimeout進行了3次渲染,而且有長時間幀出現:

setTimeout

使用requestAnimationFrameDOM操作部分合並,只進行了2次渲染,長時間幀也被優化:
requestAnimationFrame

2. DOM無關的耗時操作放到Web Worker

Web Worker的好處是什麼?
JavaScript是單執行緒的,如果頻繁的進行耗時操作(如實時更新資料),就會造成擁堵,影響使用者互動體驗。Web Worker的作用在於為JavaScript建立了多執行緒環境,worker執行緒在後臺執行,受主執行緒控制,兩者互不干擾。worker執行緒負擔高延遲且UI無關的任務,主執行緒負責UI互動就會相對流暢。
需要注意

  • Web Worker無法操作DOM,本質上只是將資料重新整理和頁面渲染拆開執行。
  • Web Worker遵循同源策略且限制本地訪問。
  • 用一次多餘的網路請求和瀏覽器執行緒資源來換取高效執行。

優化效果具體如何?DEMO
可以通過chrome的performance皮膚檢視具體表現的差別: 不使用web worker,減少了一次網路請求,但是出現了長時間幀,有卡幀的風險。

不使用worker

使用了web worker之後,耗時操作無關的任務不再被阻塞,但是增加了網路延遲。如果在專案中使用worker,初始化時間需要好好斟酌。
使用worker

可考慮的應用場景

  • 輪詢伺服器獲取資料
  • 頻繁的資料上報
  • 耗時的資料處理

Style:降低樣式計算複雜度和範圍

1. 降低樣式選擇器的複雜度?

降低樣式選擇器的複雜度是常常被提出的一個優化方法,實際上這個方法的效果比較微弱,根據Ivan Curic的文章[5]的測試方法(DEMO),在一個擁有50000個節點的頁面中,不同選擇器複雜度對於效能的影響不會超過20ms,而一般情況下,頁面的節點數都不會達到這個數量。
優化效果微弱的原因在於瀏覽器引擎對選擇器速度進行了優化,不同引擎的效能優化方案不同,所以開發者的優化是否有效是難以預測的,至少對於靜態元素的優化價效比是極低的。
通過測試可以確認的一點是,應當減少偽類選擇器和過長的選擇器的使用。推薦按照如OOCSS、BEM等命名規範來組織CSS,優點是在微弱優化效能的同時也提高了程式碼可維護性。

2. 減少需要執行樣式計算的元素個數

這一點是針對較早的瀏覽器而言,較早的瀏覽器如改變了body元素上的一個類,則其子元素都需要重新計算樣式。
現代瀏覽器都進行了優化,所以優化效果要視具體應用場景而言。目前尚未挖掘到應用例子,後期如有發現回來填坑。

Layout:避免大規模、複雜的佈局

1. 避免頻繁觸釋出局

不同的屬性導致的渲染成本不盡相同,這一點在css動畫時對比尤其明顯。觸發layout或者paint的動畫屬性尤其消耗效能,所以應當儘量使用transformopacity作為動畫屬性,如果無法實現則考慮採用JavaScript實現動畫。
效能差別有多大? 以width和transform為例,分別實現動畫的效能差別:DEMO
通過width實現動畫,幀率較低且曲線抖動明顯,右下角也給出了一幀的渲染過程,觸發了樣式計算,佈局,繪製和渲染層合併:

width實現動畫

通過transform實現動畫,可以發現幀率雖然也低但是平穩,渲染過程只觸發了樣式計算和、繪製和渲染層合併(僅當元素為合成層時,不會觸發繪製。後面將詳細講述):
transform實現動畫

2. 用flexbox佈局替代老的佈局模型

常用的經典佈局方案有基於浮動的佈局、基於絕對定位的佈局,flexbox佈局相較而言更加高效。在能用flexbox佈局的專案中,儘量用flexbox佈局。以下DEMO嘗試用三種佈局方式渲染一樣的介面效果來測試效能:
絕對佈局:對於每一個元素都需要唯一的定位座標,當元素較多時,CSS檔案偏大,導致在樣式計算上花費了較多的時間。

絕對佈局

浮動佈局:浮動元素之間定位會互相影響,部分浮動元素也受到文件流影響,導致佈局所需時間較長。
浮動佈局

彈性佈局:對比前兩種佈局方案而言,效能有較顯著的提升。
彈性佈局

3. 避免強制同步佈局事件

什麼是強制同步佈局?
前面提到了頁面渲染流程是JavaScript->Style->Layout->Paint->Composite,強制同步佈局就是強制瀏覽器在執行JavaScript指令碼前先執行佈局。
什麼情況會導致強制同步佈局?
JavaScript執行時,獲取到的元素屬性樣式都是上一幀的數值,所以如果在當前幀的渲染流程中,獲取當前幀的某個元素屬性之前對該元素進行了修改,瀏覽器就必須先應用屬性再執行JavaScript邏輯,簡而言之就是DOM先寫後讀操作,尤其是連續的讀寫操作,對瀏覽器的效能影響更大。 對效能影響有多大?DEMO
DEMO通過改變1000個節點的屬性,測試強制同步佈局事件對效能的影響,具體參照下圖。可以發現效能的損耗是極大的,連續的讀寫操作導致連續的強制同步事件觸發,JavaScript執行時間變得很長:

強制同步佈局

Paint/Composite:GPU加速

1. 將移動或漸變元素由渲染層(RenderLayer)提升為合成層(Compositing Layer)

注:可在Chrome的開發者工具的layers皮膚檢視合成層,layers皮膚開啟方法command+shift+p(mac)/ctrl+shift+p(windows) -> show layers 將複雜/頻繁變化的元素提升到合成層,這樣的好處是該元素繪製的時候不會觸發其他元素的繪製。渲染層提升為合成層的原因如下(注意以下原因是在渲染層的基礎之上):

  • 有3D transform屬性
  • 有perspective屬性
  • 3D canvas或硬體加速的2D canvas
  • 硬體加速的iframe元素(如iframe嵌入的頁面有合成層,合成層需要硬體加速)
  • 使用了硬體加速的外掛,如flash/iframe
  • 對opacity/transform屬性應用了animation/transition(當animation/transition為active)
  • will-change屬性為opacity、transform、top、left、bottom、right
  • 子元素是compositing layer
  • 兄弟元素是compositing layer,與當前的非composting layer有重疊,composting layer的層級低於非composting layer層

為什麼會有效能提升?

  • 只重繪需要重繪的部分
  • GPU加速:合成層的點陣圖直接由GPU合成,比CPU處理速度更快

效能提升有多少? DEMO 通過demo可以看到,提升為合成層之後,paint所需的時間大大減少。

render layer -> compositing layer

提升合成層是不是越多越好?
可以看到提升合成層後,paint時間大大下降。但是合成層的建立需要消耗額外的記憶體和管理資源,過多的合成層給頁面帶來的記憶體開銷很大,DEMO建立了5000個元素,全部元素都提升為合成層與不提升時的記憶體消耗進行對比。這一點在移動端尤其需要注意,相比較於PC,移動裝置的記憶體資源更加緊張。

過多合成層

只提升動畫元素的渲染層
基於提升為合成層來提升效能的原理,當頁面其他部分繪製比較複雜且相對靜態時,我們可以考慮將動畫元素單獨提升為合成層,減少動畫元素對頁面其他元素的影響。

2. 避擴音升合成層的陷阱

回顧一下提升為合成層的最後一個原因:兄弟元素是compositing layer,與當前的非composting layer有重疊,composting layer的層級低於非composting layer層。
這種情況下導致的提升合成層一般都是預期外的。其原因與螢幕的渲染流程有關,我們回憶一下頁面對映的最後一步,每一個Compositing Layer對應一張點陣圖,合成器最後將這些點陣圖根據層級關係合併起來最終輸出到螢幕。此時我們假設A是已知的合成層,而B理想中應當是普通渲染層,其層級關係如圖所示:

層級陷阱

B作為普通渲染層與父級元素位於同一張點陣圖,A單獨在一張點陣圖,此時合併的時候層級就會出現問題,如果直接將B置於A之上,有可能導致層級低於A的B的父元素反而顯示在了A之上,反之A,B的層級關係就不對了。瀏覽器此時的解決方案,就是將B也單獨出來作為compositing layer進行渲染,導致了意料外的compositing layer生成。 這種時候第一直覺就是避免重疊的發生不就好了嘛?然而事情並不簡單。在查詢資料的時候發現了一個神奇寶貝——assumedOverlap。字面意思是假設重疊,對於無法/難以判斷是否會與compositing layer重合的某些元素,瀏覽器假設會發生重疊,提升為compositing layer。
對此瀏覽器也進行了優化的,通過層壓縮(Layer Squashing)處理,將與合成層有重疊且連續多個的渲染層合併為一個合成層。防止由於重疊導致的提升合成層過多,導致的層爆炸(Layer Explosion),可參考DEMO
然而層壓縮還是有解決不了的情況,檢視原始碼可以列出以下原因(注意一下都是在重疊/假設重疊的前提下):

  • scrollsWithRespectToSquashingLayer:渲染層相對於壓縮層滾動,當滾動的渲染層與合成層重疊時,會有新的合成層生成且無法壓縮。DEMO(這個例子不是很好,codepen用iframe嵌入,整個iframe都變成了合成層,如果想看效果可以在本地看)
  • squashingSparsityExceeded:渲染層壓縮後會導致壓縮層過於稀疏。DEMO
  • squashingClippingContainerMismatch:渲染層和壓縮層的裁剪容器(clip container)不同,簡單理解就是重疊的渲染層的容器overflow型別不同。DEMO
  • squashingOpacityAncestorMismatch:渲染層與壓縮層的繼承自祖先的opacity屬性不同。DEMO
  • squashingTransformAncestorMismatch:渲染層與壓縮層的繼承自祖先的transform不同。DEMO
  • squashingFilterAncestorMismatch:渲染層與壓縮層的繼承自祖先的filter屬性不同,或者是渲染層本身有filter屬性。DEMO
  • squashingWouldBreakPaintOrder:無法在不打亂渲染順序的前提下壓縮(e.g. 父元素有mask/filter屬性,子元素與壓縮層overlap,則假如合併了,父元素的mask/filter屬性無法區域性應用在壓縮層,導致渲染結果有誤)。DEMO
  • squashingVideoIsDisallowed:video元素無法被壓縮。DEMO
  • squashedLayerClipsCompositingDescendants:當合成層是被剪下的子元素時,與之重疊的渲染層無法被壓縮。DEMO
  • squashingLayoutPartIsDisallowed:無法壓縮frame/iframe/plugin。
  • squashingReflectionDisallowed:無法壓縮有reflection屬性的渲染層。 DEMO
  • squashingBlendingDisallowed:無法壓縮有blend mode屬性的渲染層。DEMO
  • squashingNearestFixedPositionMismatch:渲染層的最近fixed元素與壓縮層不同,無法被壓縮。DEMO

當發現頁面明明沒有什麼內容卻比較卡的時候可以檢查一下是不是這個原因,以下給出常見的層壓縮解決不了的情況:

  1. transform動畫的元素,其後的元素為relative/absolute定位
    原因:relative元素和relative下的absolute元素由於assumedOverlap原因都被被提升為合成層,又由於設定了overflow:hidden,基於前面提到的squashingClippingContainerMismatch,渲染層與合成層的裁剪容器不同,導致無法層壓縮,出現過多的合成層。 解決方法:為動畫的元素設定z-index擾亂compositing layer的排序。DEMO

三、參考

本文結構主要參照文章[1],對其中的一些優化點進行了實際測試和擴充套件,也算是一篇讀後感吧~
關於層壓縮部分情況過於複雜,沒找到什麼資料,感覺還沒有完全吃透,後面有機會再重新整理一下。感恩以下大佬!

  1. 深度剖析瀏覽器渲染效能原理,你到底知道多少? www.jianshu.com/p/a32b890c2…
  2. Optimizing CSS: ID Selectors and Other Myths www.sitepoint.com/optimizing-…
  3. GPU Accelerated Compositing in Chrome www.chromium.org/developers/…
  4. GPU加速是什麼 aotu.io/notes/2017/…
  5. Blink Compositing Update: Recap and Squashing docs.google.com/presentatio…
  6. 無線效能優化:Composite taobaofed.org/blog/2016/0…

撒花完結~歡迎指教~

相關文章