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

来源:https://www.cnblogs.com/dashnowords/archive/2019/10/20/11706774.html
-Advertisement-
Play Games

示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 一. 高性能動畫 動畫的流暢程度通常是以 ( Frame Per Second ,每秒 ...


目錄

示例代碼托管在:http://www.github.com/dashnowords/blogs

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

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

一. 高性能動畫

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

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

二. 像素渲染管線

基本渲染流程

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

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

編寫在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的實例就比較容易理解軟體渲染過程中的一些策略了,很多東西你覺得不理解,並不一定是因為它本身有多複雜,只是因為你無法知道它是為瞭解決什麼問題而存在的,實際上當你面對同樣的問題時,可能也會採取類似甚至更好的處理策略,但當我們只看別人描述解決方案時,通常都會感覺到一個東西“特別複雜”或者“特別高大上”,所以請永遠保持謙遜,但也別丟了你的自信。最後分享一個最近很喜歡的冷段子,下一期再見。

問:"從前有一隻菜鳥,他特別菜,但是他仍然在飛,請問為什麼?"

答:“因為他有一顆勇敢的心!”


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 呼出快捷指令面板:cmd + shift + p 在Devtools界面下,鍵入cmd + shift + p`將其激活,然後開始在欄中鍵入要查找的命令或輸入?號以查看所有可用命令 .如下圖:其中Open file,Go to line,Go to symbol對於快速打開文件和定位文件位置還是節省 ...
  • 本文連接 https://www.cnblogs.com/aknife/p/11709255.html 一.頁面樣式 二.資料庫 三.前端頁面代碼 controller層 service層 serviceImpl層 dao層 mapper層 本文連接https://www.cnblogs.com/a ...
  • [TOC] 前一陣子遇到一個小問題,在同樣的樣式(主要是寬高邊距之類的)條件下,DIV在移動端和PC端的寬度不一樣,排除了絕大多數樣式的問題,但是有個比較陌生,就是box sinzing,其實經常看到,只不過沒怎麼註意過,連具體的值都不知道有哪些,在開發者工具裡面試了一下,果然和這個樣式有關,因此查 ...
  • 表單內容: form 表單 input 輸入框 name屬性:標簽的名字 value屬性:設置輸入框的預設內容 checked屬性:設置單選或多選的預設項 type屬性:用於確定輸入框的類型 type屬性的屬性值: text:單行文本 password:密碼 email:郵箱 radio:單選 ch ...
  • 前言 在上一篇初識Vue核心中,我們已經熟悉了vue的兩大核心,理解了Vue的構建方式,通過基本的指令控制DOM,實現提高應用開發效率和可維護性。而這一篇呢,將對Vue視圖組件的核心概念進行詳細說明。 什麼是組件呢? 組件可以擴展HTML元素,封裝可重用的HTML代碼,我們可以將組件看作自定義的HT ...
  • B站自動填彈幕(附帶createEvent消息機制) 昨晚看的比賽真的要氣死我。RNG 居然又輸了。。。 為了LPL。。。我寫了一個為LPL加油的腳本。希望大家能和我一起為LPL加油! 腳本代碼如下: 第一步打開瀏覽器並登錄B站 按F12將控制台打開 將代碼粘貼進去然後按回車 請大家觀賞效果 往下就 ...
  • 思路 自定義導航欄高度組成:狀態欄(綠色部分)、導航欄(藍色部分) 狀態欄 通過調用 wx.getSystemInfoSync 獲取 導航欄 通過獲取右上角膠囊的位置信息計算,navBarPadding為導航欄上下的間隙 代碼 app.js: wxml: wxss: js: 最後 setStatus ...
  • 1.什麼是常量? 常量表示一些固定不變的數據 現實生活中人的性別其實就可以看做是常量, 生下來是男孩一輩子都是男孩, 生下來是女孩一輩子都是女孩 2.JavaScript中常量的分類 整型常量其實就是正數, 在JavaScript中隨便寫一個整數都是整型常量1 / 666 / 99 實型常量其實就是 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...