身為一個前端,只考慮動畫怎樣實現就夠了么?也許后續的動畫性能優化才是你最大的敵人。。
為什么會有這篇博文,說來慚愧。雖然用過CSS3制作過大量的動畫效果,但在PC端和移動端,動畫表現時佳時不佳,會卡頓會掉幀,有大量動畫的頁面更是會使移動設備的耗電和發熱狀態達到跟玩高FPS大型手游一樣。小動畫的卡頓掉幀問題也夠讓人抓耳撓腮一段時日。這篇博客並不會給出解決方案(因為我也沒找到解決方案),因為導致動畫卡頓的原因數不勝數,比如低端安卓設備,縱使用transform,動畫還是有可能從直接從一邊運行一段時間...然后“瞬移”到另一邊...,未知因素過多,故只是在此記錄一下之前制作動畫時未考慮到的知識。
一、瀏覽器渲染流程
說到動畫性能,就不得不提到頁面的渲染流程
- 解析HTML,創建DOM樹
- 解析CSS,生成CSS規則樹
- 將DOM樹與CSS規則樹合並,構建渲染樹(RenderingObject樹)
- 布局和繪制,重繪(repaint)和重排(reflow) (重排也稱回流)
二、重繪和重排
對動畫性能影響最大的,就是重繪和重排。且重排的代價比重繪要大。重排的花銷跟render tree有多少節點需要重新構建有關系,假如在body最前面插入一個元素,會導致整個render tree回流,但如果是指body后面插入一個元素,則不會影響前面的元素重排。
1. 當頁面布局和幾何屬性改變時就需要重排。下述情況會發生瀏覽器重排:
- 添加或者刪除可見的DOM元素
- 元素位置改變
- 元素尺寸改變(包括:內外邊距、邊框厚度、寬度和高度等屬性的改變)
- 內容改變,例如:文本改變或者圖片被另一個不同尺寸的圖片替代
- 頁面渲染器初始化
- 瀏覽器窗口尺寸改變
- 對可見元素 display:none,或者對不可見元素 display:block 時
- 激活偽類(:hover)
- transition對寬高的處理,在整個transition的每一幀中,瀏覽器都要去重新布局,繪制頁面(參考)
根據改變的范圍和程度,渲染樹中或大或小的對應的部分也需要重新計算。有些改變會觸發整個頁面的重排:例如,當滾動條出現時。
瀏覽器重排必定導致重繪,但重繪不一定導致重排。
2. 重繪何時發生:
當 render tree 中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀、風格,而不會影響布局的,比如 background-color,則稱之為重繪。
- 改變字體
- 增加或者移除樣式表
- 內容變化,比如用戶在input框中輸入文字
- 激活CSS偽類(:hover)
- 腳本操作DOM (也有可能造成回流)
- 計算 offsetWidth 和 offsetHeight 的屬性
- 設置style屬性的值
3.渲染樹變化的排隊與刷新
由於每次重排都會產生計算損耗,大多數瀏覽器通過隊列化修改並批量執行來優化重排過程。然而你可能會(經常不知不覺)強制刷新隊列並要求計划任務立即執行。獲取布局信息的操作會導致隊列刷新,比如以下方法:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- width,height
- getComputedStyle() (currentStyle in IE)
- JS更改元素style
以上屬性和方法需要返回最新的布局信息,因此瀏覽器不得不執行渲染隊列中的“待處理”變化並觸發重排以返回正確的值。
在修改樣式的過程中,最好避免使用上面列出的屬性。它們都會刷新渲染隊列,即使你是在獲取最近未發生改變的或者與最新變化無關的布局信息。
4. 最小化重繪和重排
- 最小化DOM訪問次數,盡可能在JavaScript端處理
- 如果需要多次訪問某個DOM節點,請使用局部變量存儲它的引用
- 小心處理HTML集合,因為它實時聯系着底層文檔。把集合長度緩存到一個變量中,並在迭代中使用它
- 如果可能的話,使用速度更快的API,比如 querySelectorAll() 和 firstElementChild
- 要留意重繪和重排:批量修改樣式時,“離線”操作DOM樹,使用緩存,並減少訪問布局信息的次數
- 動畫中使用絕對定位
- 使用事件委托來減少事件處理器的數量
- 盡量避免用 transition 過渡會更改布局的屬性,如果有位移之類的,考慮用transform + transition
- 制作動畫時,盡量使用 CSS3 的 transform,因為 transform 屬性不會改變元素的布局(更詳細的知識可以參考:詳談層合成composite )
三、小動畫卡頓解決方案
之前做過一個小動畫,是一個元素的寬度由 0px 變到 40px,所經歷時長是7s。我是怎么寫的呢,常規思路:
.block{ animation: change 7s linear; } @keyframes change{ from{ margin-left: 0px; } to{ margin-left: -40px; } }
結果動畫看起來很“卡”,在我看了上述重繪、重排的知識后,以為定是自己寫的動畫性能忒差導致的,遂按照上述規則改進了一下,將.block設置為了絕對定位,使其脫離文檔流,margin-left 改成 left,無果,依然卡。
仔細一想,不對勁,雖然這個小動畫性能不佳,但是整個頁面只有這一個動畫。仔細看了下自己定義的動畫規則,7s變化40px....是否是間隔太長的緣故...遂將時長設置為4s,卡頓緩和了許多,設置時長越短,卡頓越不明顯。
但關鍵是,要求制作的效果就是要7s變化40px,遂改用transform:translateX()。終於,在7s改變40px的情況下,也能做到絲滑流暢了。
但為什么transform在7s使就可以做到絲滑流暢,這里有兩點猜想:有可能是因為transform開啟了GPU加速,有可能動畫對transform屬性和非transform屬性的渲染幀數不一樣。但到底是為何,還有待求證。
但不管是因為什么,請記住這句話,動畫配合transform食用更佳。
*參考:
《高性能的JavaScript》——Nicholas C.Zakas
