從瀏覽器渲染原理,說一說如何實現高效的動畫

寫在前面

在平時的工做中,頁面的動畫效果是很常見的需求。那麼,怎麼樣實現一個高效的動畫呢?javascript

本文首發於公衆號:符合預期的CoyPancss

注,本文談到的瀏覽器,均爲基於Chromium的現代瀏覽器。html

頁面渲染原理

一個頁面展現在用戶面前,簡單來講,會經歷以上5個步驟。咱們能夠把上面這個圖稱爲像素管道java

  • Javascript: 執行js邏輯,修改DOM,修改CSS等。
  • Style:計算樣式。
  • Layout:在知道對一個元素應用哪些規則以後,瀏覽器便可開始計算它要佔據的空間大小及其在屏幕的位置。這個步驟,就是咱們常說的重排。
  • Paint: 繪製是填充像素的過程。它涉及繪出文本、顏色、圖像、邊框和陰影,基本上包括元素的每一個可視部分。繪製通常是在多個表面(一般稱爲層)上完成的。這個步驟,就是咱們常說的重繪。
  • Composite:渲染層合併,由上一步可知,對頁面中 DOM 元素的繪製是在多個層上進行的。在每一個層上完成繪製過程以後,瀏覽器會將全部層按照合理的順序合併成一個圖層,而後顯示在屏幕上。

在瀏覽器中,頁面的渲染由瀏覽器的渲染進程完成,而渲染進程中,包含了主線程,worker線程,Compositer線程,Raster線程。上述像素管道的5個過程當中,前4個過程,都由主線程完成,最後一個步驟,主要由Raster線程、Compositer線程完成css3

JavaScript、Style、Layout

像素管道中的前三個步驟,你們都很熟悉了。JavaScript、Style兩個步驟,一圖以蔽之:web

接着是Layout,瀏覽器遍歷render tree的每個節點,計算其確切大小和位置。最終造成一個Layout Tree。canvas

Paint

在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線程完成的。

Composite

通過以前的幾個步驟,瀏覽器主線程已經將頁面的內容分紅了若干渲染層。爲了提高性能,某些特定的渲染層,會被提高爲合成層。咱們能夠經過下面兩個css屬性,將某個元素強制提高爲合成層:

will-change: transform;

// 或者
transform: translateZ(0);
複製代碼

注:提高爲合成層的條件比較複雜,這裏就不一一展開了。能夠參考這篇文章:

taobaofed.org/blog/2016/0…

主線程在處理完全部的全部的數據後,會把數據提交到Compositer線程。Composite線程會利用Raster線程來作光柵化處理,並將處理好的內容存入內存中。隨着Composite線程完成渲染層合成操做,扔給GPU,頁面最終被渲染到屏幕上。

能夠經過Chrome開發者工具中的Layer來查看合成層:

其餘運行方式的像素管道

上文中的像素管道共有5個步驟。不必定每幀都老是會通過管道每一個部分的處理。實際上,不論是使用 JavaScript、CSS 仍是網絡動畫,在實現視覺變化時,管道針對指定幀的運行還有其餘兩種方式:

第一種就是咱們所說的頁面沒有進行重排,只進行了重繪;第二種就是頁面既沒有進行重排,也沒有進行重繪。

最後這種運行方式的開銷最小,適合於頁面上的動畫效果。

實現動畫效果

不考慮canvas等,有三種常見的方式來實現頁面上的動效,

  1. 徹底不用css3相關屬性,僅使用setTimeout, setInterval, requestAnimationFrame,經過js修改DOM的樣式來實現動畫。
  2. 使用純css3來實現動畫。
  3. js與css3相結合來實現動畫。

通常狀況下,使用第一種方式的時候,雖然有的動畫效果在進行過程當中不會觸發像素管道中的Layout,可是Paint每每是避免不了的。而使用css3來實現動畫時,咱們能夠跳過Layout和Paint步驟。

下面,來看看三種實現方式下,瀏覽器的處理過程。

徹底由JS驅動的動畫

代碼以下:

<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動畫

咱們用純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在工做。

JS與CSS3相結合

<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,常見的屬性是:transformopacity。具體屬性能夠到下面的網址查看:

csstriggers.com/

這裏還有幾點補充的地方:

  1. 動畫開始時,都會觸發一次paint。

  2. 對於純css3操做transform和opacity的動畫,在動畫開始時,瀏覽器會自動將動畫元素提高爲合成層,可是在動畫結束後,合成層會失效。在動畫結束後(合成層失效)的那一幀,瀏覽器是會觸發Paint的。若是咱們強制將動畫元素提高爲合成層,動畫結束後的那一幀,就不會觸發Paint了。

  3. 對於js操做css3的transform和opacity的動畫,在動畫過程當中,瀏覽器不會自動將動畫元素提高爲合成層,可是也不會觸發Paint。在動畫結束的那一幀,無論咱們是否強制將動畫元素提高爲合成層,當頁面動畫元素嵌套複雜時,可能會觸發Paint。

總結

想要實現高性能的動畫,儘可能使用css動畫或者使用js操做css3屬性的方式,同時,要注意動畫用到的css3屬性。動畫的目標就是跳過瀏覽器的Layout和Paint,僅觸發Composite。

對於特定的動畫元素,咱們能夠適當將其提高到合成層,這樣該元素不會影響到頁面其餘地方。固然,合成層的使用要適當,由於合成層會帶來內存壓力。

寫在後面

本文從瀏覽器渲染原理入手,談到了如何實現一個高效的動畫。在寫做本文的過程當中,學習、鞏固了不少的知識。還有一些更深刻的點值得去繼續研究。符合預期。

參考資料:

相關文章
相關標籤/搜索