高性能Web動畫和渲染原理系列(2)——渲染管線和CPU渲染

示例代碼託管在:http://www.github.com/dashnowords/blogshtml

博客園地址:《大史住在大前端》原創博文目錄前端

華爲雲社區地址:【你要的前端打怪升級指南】html5

一. 高性能動畫

動畫的流暢程度一般是以FPS(Frame Per Second,每秒幀率)做爲衡量的。在攝像機錄製視頻時每一幀實際上包含了一段時間內的畫面記錄(長曝光攝影的道理相同的),若是畫面裏的事物在運動,那麼暫停播放時看到的畫面一般都是模糊的,這樣的畫面也被稱爲「模糊幀」,加上雙眼「視覺暫留」效果的影響,影視做品通常只要達到24FPS就能夠展現出看起來連續運動的畫面;而在頁面的渲染中,每一幀都是由計算機計算渲染出來的精確畫面,幀和幀之間並不存在模糊過渡,因此一般認爲須要達到50FPS~60FPS的幀率,纔可以獲得較好的觀看體驗。git

爲了達到儘量接近60FPS以上的幀率,瀏覽器每一幀的計算和繪製所花費的時間就須要控制在1000/60≈16.6ms之內,根據Google開發者社區提供的資料,開發者最好可以將全部的工做控制在10ms左右,以便給瀏覽器一些處理內部工做的時間,不然就沒法在限定的時間內完成畫面更新,動態的內容就會表現出卡頓,對用戶體驗形成負面影響。下一節就來看一下,在這16ms的時間裏,瀏覽器都須要完成哪些任務。github

二. 像素渲染管線

基本渲染流程

談起瀏覽器的工做流程,你可能會在大多數文章中見過下面這張圖:編程

它直觀地描述了瀏覽器如何將HTML文件和CSS樣式文件經過逐步處理最終合成渲染樹並展現在頁面上的過程,固然其中每一步都是很是複雜的,若是你對此還不熟悉,能夠經過【瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕】這篇文章進行了解(極力推薦這篇文章!)。但實際上上面的流程裏並無覆蓋網站的整個生命週期,它只是描述了從用戶獲取到網站首頁和資源文件後到完成首屏渲染這段時間內所作的工做,儘管工做流程幾乎是一致的,但諸如響應用戶的交互動做,在頁面上實現動畫等等內容,只經過上面的宏觀原理圖理解起來仍是很困難的。當開發者談及瀏覽器渲染性能的話題時,咱們一般會聽到「重排」、「重繪」等術語,實際上它們就是對這後半部分工做的描述,它被稱爲「瀏覽器像素渲染管線」,此時就須要祭出Google開發者社區提供的基本原理圖:canvas

編寫在JavaScript代碼中的那些事件監聽器、定時任務等等異步觸發的代碼就會在橙色的部分執行,這部分代碼運行在主線程中,若是有問題的代碼或是執行時間較長的代碼在其中形成了阻塞,後續的幾個步驟就只能等着,這會直接延緩頁面的渲染甚至致使頁面直接崩潰,當JavaScript執行完一個宏任務並清空了當前的微任務隊列後,就會開始UI渲染流程,進入下一個環節。後端

Style階段須要找出發生變動的樣式並從新計算相關的尺寸,固然在首屏渲染以前第一次處理CSS樣式時,瀏覽器確定已經對計算結果進行了緩存,以便在這像素渲染管線處理時節省時間。數組

計算完樣式自己後,就須要進入Layout階段,從新來計算髮生樣式變更的元素應該以怎樣的盒模型尺寸繪製在畫面上的哪一個位置,網頁中的基本排版遵循正常文檔流的規則,因此一個元素尺寸變化後,就有可能須要從新計算其父子元素或臨近元素的位置,不難想象這是一個極容易引起蝴蝶效應的環節。完成了Layout佈局後,能夠看到圖中使用的顏色也發生了變化,由於相對而言它們的開銷就比較輕量了。

Paint階段就是生成像素數據的過程,它會將元素的背景、邊框、陰影等等可見的部分繪製出來,它們可能會被繪製在多個層上。

Composite階段,因爲繪製階段生成的畫面可能分佈於多個層,那麼最終渲染的結果就須要將它們按照必定的順序完成畫面的重疊,這就是瀏覽在合成階段主要的工做,固然這個過程並不必定是由CPU獨自完成的,後面還會講到。當動畫執行時,瀏覽器會不斷建立幀,上面的過程就會反覆發生,從而實現幀畫面的不斷變更:

迴流和重繪

不一樣的CSS樣式的性能開銷和形成的影響是不一樣的,因此上面的像素渲染管路的各個階段並不必定都有工做要作,若是發生變動的元素樣式不會形成佈局變化,那麼layout階段就不須要作什麼工做,若是發生變動的CSS屬性也能夠不用從新計算各部分的像素顏色,那麼paint階段也就沒有什麼工做要作,這樣渲染管路就被簡化成爲:

這是咱們最指望獲得的理想狀態。若是發生變化的CSS屬性致使Layout階段任務量的增長,這類狀況就被稱爲「迴流」「重排」,若是發生變化的CSS屬性致使了Paint階段任務量的增長,這類狀況就被稱爲「重繪」,它的開銷相比Layout而言更小,從管線的特徵不難明白,「迴流」必然會致使「重繪」,但反之則不必定成立。

只經過Composite階段的工做就能夠處理的CSS屬性就是opacity(透明度)和transform(變形),它們是各種場景中優先推薦使用的性能最高的特性,transform能夠很方便地模擬出位置變化,在能夠忽略畫面精度的狀況下(例如純色的背景)也可使用scale來模擬尺寸變化。

因此在知足需求的前提下,咱們固然但願選擇改變性能開銷更小的屬性,以即可以在16ms的時間內完成整個渲染管線的任務,這裏所說的性能,一般是指持續修改樣式時的性能開銷,暫不討論低頻的頁面狀態變更。關於CSS屬相詳細的性能開銷,能夠在【CSS Triggers】查看詳情,每一個瀏覽器的實現上有細微的差異。

opacitytransform的動畫性能開銷最小,並非由於處理它們形成的影響時工做量減少了,而是由於這兩個屬性形成的影響能夠在圖層合成時能夠委託給強大的GPU來執行。GPU的基本架構和CPU不一樣,它擁有更多算術邏輯單元(也就是ALU),這使得它很是適合以並行計算的形式執行計算密集型任務,例如圖形的矩陣變換、人工神經網絡的訓練等等。

opacitytransform形成的影響,均可以經過改變圖層合成時的參數來進行處理,換句話說就是它能夠直接使用以前生成的位圖像素數據的緩存,而不須要再從新計算,也不用更新像素數據緩存,配合上GPU強大的算力,性能天然很能打。

三. 舊軟件渲染

現代瀏覽器多采用軟硬件混合渲染的方式來處理,軟件渲染的方式一般也被成爲「舊軟件渲染」(與之相對應的是硬件加速渲染),「舊」只是出現時間比較早,並不表示它已經被硬件渲染所取代。最初的網頁並非做爲完整的應用存在的,而只是用來作一些信息展現,二維渲染的場景居多(由於頁面上大多都是基於「盒模型」的矩形區域和文字包圍盒的計算和繪製),這時使用CPU渲染的性能並不低,「舊軟件渲染」一般使用底層的二維圖形繪製庫,你能夠藉助HTML Canvas 2D API來類比理解,在canvas畫板上實現的二維動畫,即便在逐幀動畫中進行覆蓋式的全畫布重繪,也可以保持較高的幀率;對3D圖形學有必定了解的小夥伴都知道,3D渲染引擎只支持點、線和三角形的繪製,因此一個矩形就至少須要2個三角形來表示(固然也但是多個),直觀感受上就是一種「殺雞用牛刀」的體驗,GPU的算力雖然很牛逼,但一般內存空間很是有限,因此最好只在必要時有節制地使用GPU

本節咱們先忘掉GPU的加速能力,來看看軟件中須要如何處理頁面渲染。下面以WebKit內核爲例來講明一下渲染的基本處理過程以及建立合成層的條件。想要進一步瞭解的小夥伴能夠嘗試閱讀朱永勝的《WebKit技術內幕》一書(不要輕易嘗試,很容易以爲本身不適合搞前端,甚至懷疑人生)。

渲染對象(RenderObject)

DOM樹解析時,瀏覽器會爲可見元素建立一個RenderObject類的實例,用於記錄繪製這個節點須要的一些信息和方法,RenderObject會依據HTML中的DOM結構生成一棵RenderObjectTree,但瀏覽器並無直接使用它來生成一張位圖畫面,由於若是這樣作的話,頁面上發生任何變化時,都須要從新計算變動的區域並更新緩存,它的確很節省空間,畢竟只須要緩存一張靜態圖片中各個像素點的顏色數據就能夠了,但節省空間的代價就是沒法節省時間,這樣的策略會加劇重複運算的負擔。

渲染層(RenderLayer)

爲了方便處理,WebKit會根據RenderObjectTree來對RenderObject進行按層分類,並最終建立一棵包含多個渲染圖層信息的RenderLayerTree(渲染層樹),兩棵樹中的節點並非一一對應的,當遍歷RenderObjectTree時,只有符合必定條件的節點(好比獲取了上下文的canvas節點、video節點、具備透明樣式的節點等等,詳細的規則會根據平臺實現不一樣可能會有變化)會建立出新的RenderLayer節點,而其餘的節點只須要添加到祖先節點上已經存在的RenderLayer節點上就能夠了。規則以下:

除了根節點之外,一個RenderLayer節點的父親,就是它對應的RenderObject節點的祖先鏈中最近的祖先,且二者所在的RenderLayer不是同一個。

根據《Webkit技術內幕》一書中的介紹,在軟件渲染中,每個RenderLayer對象都會有一個後端類,用來存儲該層繪製的結果(可是在硬件渲染中因爲合成層的存在,因此並不會爲每個RenderLayer生成後端類),你能夠把後端類簡單地理解爲結果緩存,CPU會將各個RenderLayer的結果最終渲染爲到一張位圖裏,而後交給GPU展現,合成的過程也能夠在GPU中進行,也就是硬件加速渲染,這裏再也不展開,可是僅考慮軟件渲染環節的話,RenderLayer樹就已經能夠實現目的了。用過photoshop的用戶可能會對分層這種處理形式比較熟悉,它的關鍵點就是在處理有重疊的區域時必須考慮前後順序。

直接看概念可能比較繞,作個簡單的比喻,好比碼農小強的爺爺有本身的房子,而後生了幾個孩子,這些孩子裏有的發展的比較好就本身買房單獨住處去了,發展的不太好的只能住在爺爺家裏,接着每一個孩子又生了一堆孩子,也就是小強這一輩,固然也是發展的有好有差,以碼農小強爲例,發展的好的就能夠本身買房子住,發展的很差的就得拼爹了,若是他爹有房子,就能夠住在爹家,若是很悲劇他爹也沒房子,那他就得和他爹一塊兒住到他爹的爹家裏去(說住到墳墓裏的你放學別走),RenderObjectRenderLayer的生成過程也是相似的。

四. 從canvas體會分層優點

Webkit底層的2D渲染使用Skia庫,它是相似於Canvas API的二維圖形繪製庫,爲了方便理解軟件渲染的優點,下面經過Canvas API來看看分層到底帶來了哪些變化,本例中咱們先不考慮從新計算佈局的狀況,僅考慮重繪的工做。如下圖爲例(若是不瞭解canvas動畫繪製,能夠參考筆者曾經寫的一篇相關博文【響應式編程的思惟藝術 (2)響應式Vs面向對象】):

假設在下面的分析中,地面天空是分別繪製上去的,人物和雲是能夠水平運動的,人比山距離觀察者更近。

不分層的狀況

canvas中,使用context.getImageData(x, y, width, height)方法取得畫布上對應矩形區域的像素數據,在不分層的狀況下,假設第一次渲染後,使用這個方法將畫布中的像素數據取出來存儲在backUp變量上(像素數據是一個很長的一維數組,按順序逐行存儲着畫面中每一個像素點的rgba4個值),也就是只爲最終結果創建了一份緩存,此時實際上已經丟失了一部分信息了,例如雲和天空、人和天空都有重疊的部分,而重疊部分的像素只保留了最上面一層的值。

當須要繪製逐幀動畫時,問題就來了。人物是運動的,那麼程序天然知道下一幀應該將人物繪製在什麼地方,可是若是直接繪製,原來的人物仍然會留在圖中,這樣逐幀畫下去,畫面上就會留下一排人物運動的分解畫面,這顯然是不行的;若是把人物先擦掉呢?也是不行的,這樣雖然能夠保持畫面上只有一個跑動的人物,可是由於畫面被緩存時,像素已經被覆蓋掉了,若是把人物擦掉,只從緩存的數據中,是沒法知道被擦掉的這部分像素點應該被修復成什麼樣子的,例以下圖中,緩存中是上一幀的數據復原後的圖,可是若是下一幀人物離開了原位置,原來的畫面就沒法利用緩存直接恢復了,例如上圖中紅框中的部分就留下了人物的殘影。

假設在上面的畫面中,人物的大小是100*100,緩存的像素中,其位置是(200,400),假設一幀中它平移了10個像素,那麼就能夠粗略地認爲須要更新的區域是左上角爲(200,400),寬110,高100的矩形區域。儘管這個110*100的矩形區域可能只佔了整個緩存區域的10%,也就是大部分緩存的像素點仍是有效的,但爲了修復這部分畫面,程序將不得不從新計算每一個對象的繪製結果,而後將這個區域的畫面按照層次從新繪製上去,在上面的示例中,變動區擦除後從下到上依次要繪製天空、山和人物,人物是繪製在最上層的以即可以完整顯示,人物離開後的空白像素也在重繪中被修復。

分層繪製

單幅位圖像素緩存的劣勢其實已經很明顯了,下面再來看看分層的狀況,假如上述畫面中的對象分別繪製在不一樣的canvas畫布上,那麼一共就須要5個canvas元素,因爲畫布是透明底色的,因此最終顯示結果是疊加而成的。接着爲每一個canvas層都生成像素數據的緩存,那麼在面對一樣的更新場景時,天空、地面、山和雲均可以不用操做,而只須要更新人物所在的canvas層,先將受影響的區域擦除,接着從新計算人物的繪製結果並更新單層的緩存,最後將新的結果繪製到目標位置上,相比之下,分層緩存的方案使用了更多的存儲空間來緩存繪製的像素數據,但減小了更新時的計算量,是典型的空間換時間的作法。

層的合併

顯示器上最終呈現的是一幅位圖畫面,因此即便在上面的示例中使用了5個分佈在不一樣層次的canvas標籤,實際上計算機在處理時仍然會對各層的像素數據按層進行合併計算。上面的示例中存在一個很容易發現的優化點,就是不管怎麼重繪,實際上地面的繪製結果都會擋住對應區域的天空的繪製結果,並且它們都是靜態的,因此天空的緩存數據中,與地面重疊的部分實際上沒什麼用,若是更新的區域發生在重疊區,那麼更新畫面的時候,天空層老是要先繪製一次而後再被更高層的或者地面覆蓋掉,這時候就能夠利用層合併的思想進行優化,也就是直接將天空,山和地面繪製在同個canvas上,它們總體的繪製結果緩存時只須要佔用原來1/3的空間(3張位圖變1張了),但對於後續的重繪卻不會形成影響,這樣就能夠省掉很大一部分肯定沒有用的緩存。固然上面的示例只是比較簡單的狀況,在DOM節點渲染結果的處理時有更加複雜的層劃分層合併的規則,可是優化的思想基本是同樣的。

五.小結

從直接繪製到分層繪製再到層的合併的過程,實際上就是從DOM節點到RenderObject樹再到RenderLayer樹的變換過程,利用canvas的實例就比較容易理解軟件渲染過程當中的一些策略了,不少東西你以爲不理解,並不必定是由於它自己有多複雜,只是由於你沒法知道它是爲了解決什麼問題而存在的,實際上當你面對一樣的問題時,可能也會採起相似甚至更好的處理策略,但當咱們只看別人描述解決方案時,一般都會感受到一個東西「特別複雜」或者「特別高大上」,因此請永遠保持謙遜,但也別丟了你的自信。最後分享一個最近很喜歡的冷段子,下一期再見。

問:"從前有一隻菜鳥,他特別菜,可是他仍然在飛,請問爲何?"

答:「由於他有一顆勇敢的心!」

相關文章
相關標籤/搜索