本文轉載自:衆成翻譯
譯者:文藺
連接:http://www.zcfy.cc/article/4386
原文:https://hacks.mozilla.org/2017/10/the-whole-web-at-maximum-fps-how-webrender-gets-rid-of-jankcss
Firefox Quantum 發佈在即。它帶來了許多性能改進,包括從 Servo 引入的的極速 CSS 引擎。git
但 Servo 中的很大一塊技術還沒有被 Firefox Quantum 引入,雖然已經爲期不遠。這就是WebRender,它是 Quantum Render 項目的一部分,正被添加到 Firefox 中。github
WebRender 以極速著稱,但它所作的並不是加速渲染,而是使渲染結果更加平滑。web
依靠 WebRender,咱們但願應用程序以每秒 60 幀(FPS)乃至更快的速度運行:不管顯示器有多大,頁面每幀發生多少變化。這是能夠作到的。在 Chrome 和當前版本的 Firefox 中,某些頁面卡到只有 15 FPS,而使用 WebRender 則能達到 60 FPS。編程
WebRender 是如何作到這些的呢?它從根本上改變了渲染引擎的工做方式,使其更像 3D 遊戲引擎。數組
一塊兒來看看這話怎麼說。瀏覽器
在關於 Stylo 的文章中,我討論了瀏覽器如何將 HTML 和 CSS 轉換爲屏幕上的像素,並提到大多數瀏覽器經過五個步驟完成此操做。緩存
能夠將這五個步驟分紅兩部分來看。前一部分基本上是在構建計劃:渲染器將 HTML 和 CSS 以及視口大小等信息結合起來,肯定每一個元素應該長成什麼樣(寬度,高度,顏色等)。最終獲得的結果就是幀樹 (frame tree),又稱做渲染樹(render tree)。網絡
另外一部分是繪製與合成(painting and compositing),這正是渲染器的工做。渲染器將前一部分的結果轉換成顯示在屏幕上的像素。數據結構
對同一個網頁來講,這個工做不是隻作一次就夠,而必須反覆進行。一旦網頁發生變化(如某個 div 發生切換 ),瀏覽器需再次經歷這當中的不少步驟。
即使頁面並未發生變化(如頁面滾動,或某些文本高亮),瀏覽器仍需進行第二部分中的某些步驟,接着在屏幕上繪製新的內容。
想要滾動、動畫等操做看起來流暢,必須以 60 幀每秒的速度進行渲染。
每秒幀數(FPS)這個術語,也許你早有耳聞,但可能不肯定其意義。想象你手上有一本手翻書(Flip Book)。一本畫滿靜態繪畫的書,用手指快速翻轉,畫面看起來就像動起來了。
爲了使這本手翻書的動畫看起來平滑,每秒須要翻過 60 頁。
這本書的是由圖紙製成的。紙上有許許多多的小方格,每一個方格只能填上一種顏色。
渲染器的工做就是給圖紙中的方格填色。填滿圖紙中的全部方格,一幀的渲染就完成了。
固然,計算機當中並不存在真實的圖紙。而是一段名爲幀緩衝區(frame buffer)的內存。幀緩衝區中的每一個內存地址就像圖紙中的一個方格...它對應着屏幕上的像素。瀏覽器將使用數字填充每一個位置,這些數字表明 RGBA(紅、綠、藍以及 alpha 通道)形式的顏色值。
當顯示器須要刷新時,將會查詢這一段內存。
多數電腦顯示器每秒會刷新 60 次。這就是瀏覽器嘗試以每秒 60 幀的速度渲染頁面的緣由。這意味着瀏覽器有16.67 ms 的時間來完成全部工做(CSS 樣式,佈局,繪製),並使用像素顏色填充幀緩衝區內存。兩幀之間的時間(16.67ms)被稱爲幀預算(frame budget)。
有時你可能聽到人們談論丟幀的問題。所謂丟幀,是系統未能在幀預算時間內未完成工做。緩衝區顏色填充工做還沒有完成,顯示器就嘗試讀取新的幀。這種狀況下,顯示器會再次顯示舊版的幀信息。
丟幀就像是從手翻書中撕掉一個頁面。這樣一來,動畫看上去就像消失或跳躍同樣,由於上一頁和下一頁之間的轉換頁面丟失了。
所以要確保在顯示器再次檢查前將全部像素放入幀緩衝區。來看看瀏覽器之前是如何作的,後來又發生了哪些變化。從中能夠發現提速空間。
注意:繪製與合成是不一樣渲染引擎之間最爲不一樣的地方。_單一平臺瀏覽器(Edge 和 Safari)的工做方式與跨平臺瀏覽器(Firefox 和 Chrome)有所不一樣。_
即使是最先的瀏覽器也有一些優化措施,使頁面渲染速度更快。例如在滾動頁面的時候,瀏覽器會保留仍然可見的部分並將其移動。而後在空白處中繪製新的像素。
搞清楚發生變化的內容,只更新變更的元素或像素,這個過程稱爲失效處理(invalidation)。
後來,瀏覽器開始應用更多的失效處理技術,如矩形失效處理(rectangle invalidation)。矩形失效處理技術能夠找出屏幕中包圍每一個發生改變的部分的最小矩形。而後只需重繪這些矩形中的內容。
頁面變化不大時,這確實可以減小大量工做。好比說,光標閃動。
但若是頁面大部份內容發生變化,這就不夠用了。因此又出現了處理這些狀況的新技術。
當頁面的大部分發生變化時,使用圖層(layer)會方便不少...至少在某些狀況下是如此。
瀏覽器中的圖層很像 Photoshop 中的圖層,或手繪動畫中使用的洋蔥皮層。大致說來就是在不一樣圖層上繪製不一樣元素。而後能夠調整這些圖層的相對層級關係。
這些一直以來就是瀏覽器的一部分,但並不老是用於加速。起初,它們只是用來確保頁面正確呈現d。它們對應於堆疊上下文(stacking contexts)。
例如一個半透明元素將在本身的堆疊上下文中。這意味着它有本身的圖層,因此你能夠將其顏色與下面的顏色混合。一幀完成後,這些圖層就被丟棄。在下一幀中,全部圖層將再次重繪。
可是,這些圖層中的東西在不一樣幀之間經常沒有變化。想一下那種傳統的動畫。背景不變,只有前景中的字符發生變化。保留並重用背景圖層,效率會更高。
這就是瀏覽器所作的。它保留了這些圖層。而後瀏覽器能夠僅重繪已經改變的圖層。在某些狀況下,圖層甚至沒有改變。它們只須要從新排列:例如動畫在屏幕上移動,或是某些內容發生滾動。
組織圖層的過程稱爲合成。合成器(compositor)從這兩部分開始:
首先,合成器將背景複製到目標位圖中。
而後找到可滾動內容中應該展現的部分。將該部分複製到目標位圖。
這減小了主線程的繪製量。但這意味着主線程須要花費大量時間進行合成。而還有不少工做在主線程上爭奪時間。
之前我已經談過這個問題,主線程有些像一個全棧開發者。它負責 DOM,佈局和 JavaScript。而且還負責繪製與合成。
主線程花費多少毫秒進行繪製、合成,就有多少毫秒沒法用於 JavaScript 和佈局。
而另外一部分硬件正在閒置,沒有多少工做要作。這個硬件是專門用於圖形的。它就是 GPU。自 90 年代末以來,遊戲一直在使用 GPU 加速渲染幀。自那之後,GPU 日益強大。
因此瀏覽器開發者開始把事情轉移給 GPU 來處理。
有兩項任務能夠轉交給 GPU:
1. 圖層繪製
2. 圖層合成
將繪製工做交給 GPU 可能比較棘手。因此在多數狀況下,跨平臺瀏覽器依然經過 CPU 進行繪製。
但 GPU 能夠很快完成合成工做,轉移過來比較簡單。
一些瀏覽器在這種並行方法上走得更遠,直接在 CPU 上添加了一個合成器線程。由它管理 GPU 中發生的合成工做。這意味着若是主線程正在執行某些操做(如運行 JavaScript),則合成器線程仍然能夠處理其餘工做,如在用戶滾動時滾動內容。
這樣就將全部合成工做從主線程中移出。儘管如此,它仍然在主線程上留下了大量的工做。圖層須要重繪時,主線程須要執行繪製工做,而後將該圖層轉移給 GPU。
有些瀏覽器將繪製工做移動到另外一個線程中(目前 Firefox 正致力於此)。但將繪製這點工做轉移到 GPU 上,速度會更快。
所以,瀏覽器也開始將繪製工做轉移到 GPU。
這項轉變工做仍在進行中。一些瀏覽器一直經過 GPU 繪製,另外一些瀏覽器只能在某些平臺上(如 Windows 或移動設備)這麼作。
GPU 繪製可以解決一些問題。CPU 得以解放,專心處理 JavaScript 和佈局g'z。此外,GPU 繪製像素比 CPU 快得多,所以它能夠加快繪製速度。這也意味着從 CPU 複製到 GPU 的數據要更少了。
可是,在繪製與合成工做之間保持這種區分仍然會產生必定的成本,即便它們都在 GPU 上進行。這麼區分,還限制了可以採用的優化的種類,它們可使 GPU工做更快。
這就是WebRender 所要解決的問題。它從根本上改變了渲染方式,消除了繪製和合成之間的區別。這種解決渲染器性能的方法,可以在當下網絡中提供最佳用戶體驗,併爲將來網絡提供最好的支持。
這意味着,咱們要作的不只僅是想使幀渲染更快...咱們但願使渲染更加一致,不會發生閃動。即使有大量須要繪製的像素,如 4k 顯示器或 WebVR 設備,咱們仍但願體驗可以平滑一些。
在某些狀況下,上述優化可以加速頁面渲染。當頁面上沒有太多變化時(如只有光標在閃爍),瀏覽器將進行儘可能少的工做。
將頁面分紅圖層,拓展了最佳情形數量。繪製數個圖層,並讓它們相對於彼此移動,則「繪畫+合成」架構效果很是好。
但圖層的使用也須要有所權衡。這將佔用很多內存,實際可能會減慢工做。瀏覽器須要組合有意義的圖層。可是很難區分怎樣是有意義的。
這意味着,若是頁面中有不少不一樣的東西在移動,圖層可能會過多。這些圖層佔滿內存,須要花費很長時間才能傳輸到合成器。
另外一些時候,須要多個圖層時,卻可能只獲得一個圖層。這個圖層將會不斷重繪並轉移到合成器,進行合成工做而不改變任何東西。
這意味着你已經將繪製量翻了一番,每一個像素都處理了兩遍,毫無益處。跨過合成這一步,直接呈現頁面會更快。
還有不少狀況下,圖層用處不大。如對背景色使用動畫效果,則整個圖層都必須重繪。這些圖層只能幫助少許的 CSS 屬性。
即便大部分幀都是最佳情形(也就是說,它們只佔用了幀預算的一小部分), 動做仍可能不穩定。只要三兩幀落入最壞狀況,就會產生可感知的閃動。
這些狀況稱爲性能懸崖(performance cliffs)。應用程序一直平穩運行,直到遇到這些最壞狀況(如背景色動畫),幀率瞬間瀕臨邊緣。
不過,這些性能懸崖是能夠規避的。
如何作到這一點呢?緊隨3D 遊戲引擎的腳步。
若是中止嘗試猜想須要什麼圖層呢?若是移除繪製與合成之間邊界,僅考慮每一幀繪製像素呢?
這聽起來彷佛很荒謬,但實際有先例可循。現代視頻遊戲從新繪製每一個像素,而且比瀏覽器更可靠地保持每秒 60 幀。他們以一種意想不到的方式作到了這一點...他們只是重繪整個屏幕,無需建立那些用於最小化繪製內容的失效處理矩形和圖層。
這樣渲染網頁不會更慢嗎?
若是在 CPU 上繪製的話,的確會更慢。但 GPU 就是用來作這事的。
GPU 正是用於進行極端並行處理的。我在上一篇關於 Stylo 的文章中談到過並行的問題。經過並行,機器能夠同時執行多種操做。它能夠一次完成的任務數量,取決於內核數量。
CPU 一般有 2 到 8 個內核。GPU 每每至少有幾百個內核,一般有超過 1,000 個內核。
雖然這些內核的工做方式有所不一樣。它們不能像 CPU 內核那樣徹底獨立地運行。相反,它們一般一塊兒工做,在數據的不一樣部分執行相同指令。
填充像素時, 咱們正須要這樣。每一個像素能夠由不一樣的內核填充。一次可以操做數百個像素,GPU 在像素處理方面上比 CPU 要快不少...當全部內核都在工做時確實如此。
因爲內核須要同時處理相同的事情,所以 GPU 具備很是嚴格的步驟,它們的 API 很是受限。咱們來看看這是如何工做的。
首先,你須要告訴 GPU 須要繪製什麼。這意味着給它傳遞形狀,並告知如何填充。
要達到目的,首先將繪圖分解成簡單形狀(一般是三角形)。這些形狀處於 3D 空間中,因此一些形狀能夠在其餘形狀背後。而後將三角形全部角頂點的 x、y、z 座標組成一個數組。
而後發出一個繪圖調用 —— 告訴GPU來繪製這些形狀。
接下來由 GPU 接管。全部的內核將同時處理同一件事情。它們會:
最後一步能夠經過不一樣的方式完成。要告訴 GPU 如何處理,能夠傳給 GPU 一個稱爲像素着色器的程序。像素着色是 GPU 中可編程的幾個部分之一。
一些像素着色器很簡單。例如形狀是單一顏色的,則着色器程序只須要爲形狀中的每一個像素返回同一個顏色。
另一些狀況更復雜,例若有背景圖像的時候,須要搞清楚圖像對應於每一個像素的部分。能夠像藝術家縮放圖像同樣…在圖像上放置一個網格,與每一個像素相對應。這樣一來,只需知道某個像素所對應的區域,而後對該區域進行顏色取樣便可。這被稱爲紋理映射(texture mapping),由於它將圖像(稱爲紋理)映射到像素。
針對每一個像素,GPU 會調用像素着色器程序。不一樣內核能夠同時在不一樣的像素上並行工做,可是它們都須要使用相同的像素着色器程序。命令 GPU 繪製形狀時,你會告訴它使用哪一個像素着色器。
對幾乎全部網頁來講,頁面的不一樣部分將須要使用不一樣的像素着色器。
在一次繪製中,着色器會做用域全部形狀,因此一般須要將繪製工做分爲多個組。這些稱爲批處理(batches)。爲了儘量利用全部內核,建立必定數量的批處理工做,每一個批次包括大量形狀。
這就是 GPU 如何在數百或數千個內核上切分工做的。正是由於這種極端的並行性,咱們才能想到在每一幀中渲染全部內容。即使有這樣極端的並行性,要作的工做仍是不少。解決起來還須要費些腦筋。該 WebRender 出場了……
回過頭再看下瀏覽器渲染網頁的步驟。這裏將產生兩個變化。
1. 繪製與合成之間再也不有區別。它們都是同一步驟的一部分。GPU 根據傳遞給它的圖形 API 命令同時執行它們。
2. 佈局步驟將產生一種不一樣的數據結構。以前是幀樹(或 Chrome 中的渲染樹)。如今將產生一個顯示列表(display list)。
顯示列表是一組高級繪圖指令。它告訴咱們須要繪製什麼,並不指定任何圖形 API。
每當有新東西要繪製時,主線程將顯示列表提供給 RenderBackend,這是在 CPU 上運行的 WebRender 代碼。
RenderBackend 的工做是將這個高級繪圖指令列表轉換成 GPU 須要的繪圖調用,這些繪圖調用被分在同一批次,加快運行速度。
而後,RenderBackend 將把這些批次傳遞給合成器線程,合成器線程再將它們傳遞給 GPU。
RenderBackend 傳遞給 GPU 的繪圖調用須要儘量快運行。它爲此使用了幾種不一樣的技術。
節省時間的最好辦法是什麼都不作。
首先,RenderBackend 能夠減小顯示列表項目。它會識別哪些項目將真正出如今屏幕上。爲此,它將查看一些東西,如每一個滾動盒的滾動距離。
若是形狀的某些部分在盒子內,則該形狀將被包括在須要繪製的列表中。不然將被刪除。這個過程叫作早期剔除。
如今有了一個樹狀結構,其中只包含將要用到的形狀。這個樹被組織成此前提過的堆疊上下文。
CSS filter 和堆疊上下文等這些效果,讓事情變得複雜了。假設有一個透明度爲 0.5 的元素,該元素包含子元素。你可能以爲每一個子元素都將是透明的……但實際上整個組纔是透明的。
所以須要先將該組渲染爲一個紋理,每一個子元素都是不透明的。而後,將子元素加入到父元素中時,能夠更改整個紋理的透明度。
這些堆疊上下文能夠嵌套...該父元素多是另外一個堆疊上下文的一部分。這意味着它必須被渲染成另外一箇中間紋理……
爲這些紋理建立空間代價不菲。咱們想盡量將事物分組到相同的中間紋理。
爲了幫助 GPU 執行此操做,須要建立一個渲染任務樹。有了它,就可以知道在其餘紋理以前須要建立哪些紋理。任何不依賴於其餘紋理的紋理均可以在首次建立,這意味着它們能夠與那些中間紋理中組合在一塊兒。
因此在上面的例子中,咱們先輸出 box shadow 的一個角。(實際比這更復雜一點,但這是要點)。
第二遍的時候,能夠將這個角經過鏡像放置到盒子的各個部分。而後就能夠徹底不透明地渲染該組。
接下來,咱們須要作的就是改變這個紋理的不透明度,並將其放在須要輸入到屏幕的最終紋理中。
經過構建這個渲染任務樹,能夠找出須要使用的離屏渲染目標的最小數量。這很好,前面已經提到過,爲這些渲染目標紋理建立空間的代價不菲。
這也有利於分批處理。
前面已經提到過,須要建立必定量的批處理,每一個批處理中包括大量形狀。
注意,建立批處理的方式真的能影響速度。同一批次中的形狀數量要儘量多。這是由幾個緣由決定的。
首先,當 CPU 告訴 GPU 進行繪圖調用時,CPU 必須作不少工做。它須要作不少工做,如啓動 GPU,上傳着色器程序和測試硬件 bug 等。而且當 CPU 進行這項工做時,GPU 多是空閒的。
其次,改變狀態是會產生代價的。假設你須要在批處理之間更改着色器程序。在典型的 GPU 上,你須要等到全部內核都使用當前的着色器完成工做後。這被稱管道清空(draining the pipeline)。管道清空後,其餘核心纔會處於閒置狀態。
所以,批處理包含的東西要儘量多。對於典型的 PC,每幀須要有100 次或更少的繪圖調用,每次調用中有數千個頂點。這樣就能充分利用並行性。
從渲染任務樹能夠找出可以批處理的內容。
目前,每種類型的圖元都須要一種着色器。例如邊框着色器,文本着色器,圖像着色器。
咱們認爲能夠將不少着色器結合起來,這樣就可以增長批處理容量。但目前這樣已經至關不錯了。
已經能夠準備將它們發送給 GPU 了。但其實還能夠作一些排除工做。
大多數網頁中都有大量相互重疊的形狀。例如,文本框位於某個帶有背景的 div 之中,而該 div 又在帶有另外一個背景的 body 中。
GPU 在計算每一個像素的顏色時,可以計算出每一個形狀中的像素顏色。但只有頂層纔會顯示。這被稱爲 overdraw,它浪費了 GPU 時間。
因此咱們能夠先渲染頂部的形狀。繪製下一個形狀時,遇到同一像素,先檢查是否已經有值。若是有值,則跳過。
不過這有一點點問題。當形狀是半透明的時候,須要混合兩種形狀的顏色。爲了讓它看起來正確,須要從裏向外繪製。
因此須要把工做分紅兩道。首先作不透明的一道工做。由表及裏,渲染全部不透明的形狀。跳過位於其餘像素背後的像素。
而後處理半透明形狀。工做由內向外進行。若是半透明像素落在不透明像素的頂部,則會混合到不透明的像素中。若是它會落在不透明形狀以後,則忽略計算。
將工做分解爲不透明和 alpha 通道兩部分,跳過不須要的像素計算,這個過程稱爲 Z-剔除(Z-culling)。
這看起來只是一個簡單的優化,但對咱們來講已是很大的成功了。在典型的網頁上,該工做大大減小了咱們須要處理的像素數量,目前咱們正在研究如何將更多的工做轉移到不透明這一步。
到目前爲止,咱們已經準備好了一幀的內容。咱們已經儘量地減小了工做。
咱們準備好啓動 GPU 並渲染各個批次了。
CPU 仍然須要作一些繪製工做。例如,咱們仍然使用 CPU 渲染文本塊中的字符(稱爲字形,glyphs)。在 GPU 上也能夠執行此操做,可是很難得到與計算機在其餘應用程序中呈現的字形相匹配的像素效果。因此 GPU 渲染的字體看起來會有一種錯亂感。咱們正在嘗試經過 Pathfinder 項目將字形等工做轉移到 GPU 上。
這些內容目前是被 CPU 繪製成位圖的。而後把它們上傳到 GPU 的紋理緩存中。這個緩存在不一樣幀之間被保留,由於它們一般不會改變。
雖然這種繪製工做是由 CPU 完成的,但速度仍有提高空間。例如,使用某種字體繪製字符時,咱們會將不不一樣的字符分割開,使用不一樣內核分別渲染。這和Stylo 用來並行計算樣式的技術是相同的……參見這裏。
在 Firefox Quantum 發佈以後的若干版本後,WebRender 有望在 2018 年做爲Quantum Render 項目的一部分,出如今 Firefox 中。這將使當今的網頁運行更順暢。隨着屏幕上的像素數量的增長,渲染性能變得愈來愈重要,所以 WebRender 還可讓 Firefox 爲新一波的高分辨率 4K 顯示器作好準備。
但 WebRender 不只僅適用於 Firefox。它對於正在開展的 WebVR 的工做一樣相當重要,在 WebVR 中,須要爲在 4K 顯示器上以 90 FPS 的速度爲每隻眼睛渲染不一樣的幀。
WebRender 的早期版本目前能夠經過 Firefox 的 flag 來啓用。集成工做仍在進行中,因此性能目前還不如工做完成hou那麼好。若是你想跟進 WebRender 開發,能夠關注GitHub repo,或者關注Firefox Nightly 的Twitter,以得到 Quantum Render 項目的更新週報。
Lin 是 Mozilla 開發者關係團隊的工程師。她在鼓搗 JavaScript,WebAssembly,Rust 和 Servo,以及繪製代碼漫畫。