在平時的工做中,頁面的動畫效果是很常見的需求。那麼,怎麼樣實現一個高效的動畫呢?javascript
本文首發於公衆號:符合預期的CoyPancss
注,本文談到的瀏覽器,均爲基於Chromium的現代瀏覽器。html
一個頁面展現在用戶面前,簡單來講,會經歷以上5個步驟。咱們能夠把上面這個圖稱爲像素管道。java
在瀏覽器中,頁面的渲染由瀏覽器的渲染進程完成,而渲染進程中,包含了主線程,worker線程,Compositer線程,Raster線程。上述像素管道的5個過程當中,前4個過程,都由主線程完成,最後一個步驟,主要由Raster線程、Compositer線程完成。css3
像素管道中的前三個步驟,你們都很熟悉了。JavaScript、Style兩個步驟,一圖以蔽之:web
接着是Layout,瀏覽器遍歷render tree的每個節點,計算其確切大小和位置。最終造成一個Layout Tree。canvas
在Paint以前,瀏覽器會根據Layout Tree,肯定須要繪製的對象的層級,咱們能夠把這個層級叫作渲染層,最終生成Layer Tree。這個階段被稱做:Update Layer Tree瀏覽器
在Paint這個階段,瀏覽器會根據Layer Tree,生成Paint Records。css3動畫
Paint Records就是描述先畫什麼,再畫什麼的記錄,跟咱們寫canvas代碼時很像。Paint Records是根據渲染層劃分的的。來看一個Paint Records的實例:網絡
儘管生成了Pain Records,真正的繪製並不在Paint這個階段完成的,而是在Composite階段由Raster線程完成的。
通過以前的幾個步驟,瀏覽器主線程已經將頁面的內容分紅了若干渲染層。爲了提高性能,某些特定的渲染層,會被提高爲合成層。咱們能夠經過下面兩個css屬性,將某個元素強制提高爲合成層:
will-change: transform;
// 或者
transform: translateZ(0);
複製代碼
注:提高爲合成層的條件比較複雜,這裏就不一一展開了。能夠參考這篇文章:
主線程在處理完全部的全部的數據後,會把數據提交到Compositer線程。Composite線程會利用Raster線程來作光柵化處理,並將處理好的內容存入內存中。隨着Composite線程完成渲染層合成操做,扔給GPU,頁面最終被渲染到屏幕上。
能夠經過Chrome開發者工具中的Layer來查看合成層:
上文中的像素管道共有5個步驟。不必定每幀都老是會通過管道每一個部分的處理。實際上,不論是使用 JavaScript、CSS 仍是網絡動畫,在實現視覺變化時,管道針對指定幀的運行還有其餘兩種方式:
第一種就是咱們所說的頁面沒有進行重排,只進行了重繪;第二種就是頁面既沒有進行重排,也沒有進行重繪。
最後這種運行方式的開銷最小,適合於頁面上的動畫效果。
不考慮canvas等,有三種常見的方式來實現頁面上的動效,
通常狀況下,使用第一種方式的時候,雖然有的動畫效果在進行過程當中不會觸發像素管道中的Layout,可是Paint每每是避免不了的。而使用css3來實現動畫時,咱們能夠跳過Layout和Paint步驟。
下面,來看看三種實現方式下,瀏覽器的處理過程。
代碼以下:
<html>
<head>
<style type="text/css"> #test2 { margin-top: 100px; width: 100px; height: 100px; position: relative; background-color: black; } </style>
</head>
<body>
<p>
這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字
</p>
<div id="test2"></div>
<script type="text/javascript"> window.onload = function() { const el = document.getElementById('test2'); let left = 0; const startTimeStamp = Date.now(); const fn = function() { left += 2; if(Date.now() - startTimeStamp > 2000) { return; } el.style.left = left + 'px'; return window.requestAnimationFrame(fn); } window.requestAnimationFrame(fn) } </script>
</body>
</html>
複製代碼
選取動畫過程當中的一幀,瀏覽器的處理過程以下:
能夠看到,在這裏幀裏,瀏覽器走完了完整的像素管道:JavaScript ->Style->Layout->Paint->Composite。
咱們用純css來實現動畫:
<html>
<head>
<style type="text/css"> #test2 { margin-top: 100px; width: 100px; height: 100px; position: relative; background-color: black; animation: move 2s; animation-fill-mode: forwards; } @keyframes move { 0% { transform: translate(0); } 100% { transform: translate(200px); } } </style>
</head>
<body>
<p>
這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字
</p>
<div id="test2"></div>
</body>
</html>
複製代碼
咱們來看看動畫進行過程當中:
能夠看到,主線程裏沒有任務在執行,而Composite線程、Raster線程以及GPU在工做。
<html>
<head>
<style type="text/css"> #test2 { margin-top: 100px; width: 100px; height: 100px; position: relative; background-color: black; } </style>
</head>
<body>
<p>
這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字
</p>
<div id="test2"></div>
<script type="text/javascript"> window.onload = function() { const el = document.getElementById('test2'); let left = 0; const startTimeStamp = Date.now(); const fn = function() { left += 2; if(Date.now() - startTimeStamp > 2000) { return; } el.style.transform = `translate(${left}px)`; return window.requestAnimationFrame(fn); } window.requestAnimationFrame(fn); } </script>
</body>
</html>
複製代碼
動畫運行時,瀏覽器的處理過程以下圖所示,並無觸發Layout和paint。
從上面的幾個實例能夠看到,在僅使用css動畫時,動畫過程徹底交由Composite線程處理,釋放了主線程。事實上,在執行純css3動畫時,瀏覽器會將響應的元素提高到一個單獨的合成層,不會影響到頁面上的其餘元素。
使用js操做css3屬性,也能夠跳過Layout和Paint。
固然,並非全部的css屬性均可以跳過Layout和Paint僅觸發Composite,常見的屬性是:transform
和opacity
。具體屬性能夠到下面的網址查看:
這裏還有幾點補充的地方:
動畫開始時,都會觸發一次paint。
對於純css3操做transform和opacity的動畫,在動畫開始時,瀏覽器會自動將動畫元素提高爲合成層,可是在動畫結束後,合成層會失效。在動畫結束後(合成層失效)的那一幀,瀏覽器是會觸發Paint的。若是咱們強制將動畫元素提高爲合成層,動畫結束後的那一幀,就不會觸發Paint了。
對於js操做css3的transform和opacity的動畫,在動畫過程當中,瀏覽器不會自動將動畫元素提高爲合成層,可是也不會觸發Paint。在動畫結束的那一幀,無論咱們是否強制將動畫元素提高爲合成層,當頁面動畫元素嵌套複雜時,可能會觸發Paint。
想要實現高性能的動畫,儘可能使用css動畫或者使用js操做css3屬性的方式,同時,要注意動畫用到的css3屬性。動畫的目標就是跳過瀏覽器的Layout和Paint,僅觸發Composite。
對於特定的動畫元素,咱們能夠適當將其提高到合成層,這樣該元素不會影響到頁面其餘地方。固然,合成層的使用要適當,由於合成層會帶來內存壓力。
本文從瀏覽器渲染原理入手,談到了如何實現一個高效的動畫。在寫做本文的過程當中,學習、鞏固了不少的知識。還有一些更深刻的點值得去繼續研究。符合預期。
參考資料: