現代瀏覽器探祕(part3):渲染

前端先鋒發表於2019-01-15

翻譯:瘋狂的技術宅 原文:developers.google.com/web/updates…

現代瀏覽器探祕(part1):架構

現代瀏覽器探祕(part2):導航

渲染器程式的內部工作原理

這是關於瀏覽器內部工作原理系列的第3部分。 之前,我們介紹了多程式架構導航流程。 在這篇文章中,我們將看看渲染器程式內部發生了什麼。

渲染程式涉及Web效能的諸多方面。 由於渲染程式中發生了很多事情,因此本文不能一一贅述。 如果你想深入挖掘,可以在Web基礎的效能部分找到更多內容。

渲染器程式處理Web內容

渲染器程式負責選項卡內發生的所有事情。 在渲染器程式中,主執行緒處理你為使用者編寫的大部分程式碼。 如果你使用了web worker 或 a service worker,有時JavaScript程式碼的一部分將由工作執行緒處理。 排版和柵格執行緒也在渲染器程式內執行,以便高效、流暢地呈現頁面。

渲染器程式的核心工作是將HTML、CSS和JavaScript轉換為使用者可以與之互動的網頁。

圖1:渲染器程式內部有主執行緒、工作執行緒、排版執行緒和柵格執行緒

圖1:渲染器程式內部有主執行緒、工作執行緒、排版執行緒和柵格執行緒

解析

構建DOM

當渲染器程式收到導航的提交訊息並開始接收HTML資料時,主執行緒開始解析文字字串(HTML)並將其轉換為文件物件模型(DOM—Document Object Model )。

DOM是頁面在瀏覽器中的內部表示,同時也是Web開發人員可以通過 JavaScript 與之互動的資料結構和API。

HTML標準將HTML文件解析為DOM。 你可能已經注意到,將HTML提供給瀏覽器從不會引發錯誤。 例如,缺少結束</ p>標記是有效的HTML。 像 Hi! <b>I'm <i>Chrome</b>!</i> 這樣的錯誤標記(b標籤在i標籤之前被關閉)被看作是 Hi! <b>I'm <i>Chrome</i></b><i>!</i>。 這是因為HTML規範旨在優雅地處理這些錯誤。 如果你對如何完成這些工作感到好奇,可以閱讀HTML規範中的“解析器中的錯誤處理和奇怪情況介紹”部分。

子資源載入

網站通常使用影像、CSS和JavaScript等外部資源。 這些檔案需要從網路或快取中載入。 主執行緒可以在解析構建DOM時會逐個請求它們,但為了加快速度,“預載入掃描器”也會同時執行。 如果HTML文件中存在<img><link>之類的內容,則預載入掃描器會檢查由HTML解析器生成的標記,並在瀏覽器程式中向網路執行緒傳送請求。

圖2:主執行緒解析HTML並構建DOM樹

圖2:主執行緒解析HTML並構建DOM樹

JavaScript可以阻止解析

當HTML解析器找到<script>標記時,它會暫停解析HTML文件,並且必須載入、解析和執行JavaScript程式碼。 為什麼要這樣處理? 因為JavaScript可以使用像document.write() 那樣改變整個DOM結構的東西來改變文件的形狀(HTML規範中的解析模型概述有一個很好的示意圖)。 這就是HTML解析器在重新解析HTML文件之前必須等待JavaScript執行的原因。 如果你對JavaScript執行中發生的事情感到好奇,V8團隊的部落格對此進行了討論。

提示瀏覽器如何載入資源

Web開發人員可以通過多種方式向瀏覽器傳送提示,以便很好地載入資源。 如果你的JavaScript不使用 document.write(),則可以向<script>標記新增asyncdefer屬性。 然後,瀏覽器非同步載入和執行JavaScript程式碼,不會阻止解析。 如果合適,你也可以使用JavaScript模組<link rel ="preload">是一種通知瀏覽器當前導航肯定需要這個資源的方法,你希望儘快下載。 你可以在資源優先順序找到更多資訊。

樣式表計算

擁有DOM不足以知道頁面的外觀,因為我們可以在CSS中設定頁面元素的樣式。 主執行緒解析CSS並確定每個DOM節點的計算樣式。 這是有關基於CSS選擇器將哪種樣式應用於每個元素的資訊。 你可以在瀏覽器中開發者工具中的computed部分中看到此資訊。

圖3:主執行緒解析CSS以新增計算樣式

圖3:主執行緒解析CSS以新增計算樣式

即使你不提供任何CSS,每個DOM節點都具有計算樣式。比如 <h1>標籤的顯示要大於<h2>標籤,同時為每個元素定義邊距。 這是因為瀏覽器具有預設樣式表。 如果你想知道Chrome的預設CSS是什麼樣的,你可以在此處檢視原始碼

佈局

現在,渲染器程式知道每個節點的文件和樣式的結構,但這還不足以呈現頁面。 想象一下,你正試圖通過手機向朋友描述一幅畫: “有一個大的紅色圓圈和一個小的藍色方塊” 這並不能完全讓你的朋友瞭解這幅畫的外觀。

圖4:一個人站在一幅畫,通過電話線與另一個人聯絡

圖4:一個人站在一幅畫,通過電話線與另一個人聯絡

佈局是查詢元素幾何的過程。 主執行緒遍歷DOM並計算樣式和建立佈局樹,其中包含x y座標和邊界框大小等資訊。 佈局樹可以是與DOM樹類似的結構,但它僅包含與頁面上可見內容相關的資訊。 如果display:none,則該元素不是佈局樹的一部分(但是在佈局樹中包含visibility:hidden的元素)。 類似地,如果應用具有類似p::before {content:"Hi!}之類的內容的偽類,則它將包含在佈局樹中,即使它不在DOM中。

圖5:主執行緒通過DOM樹生成計算樣式和佈局樹

圖5:主執行緒通過DOM樹生成計算樣式和佈局樹

確定頁面佈局是一項具有挑戰性的任務。 即使是最簡單的頁面佈局,如從上到下的塊流,也必須考慮字型的大小以及在哪裡劃分它們,因為它們會影響段落的大小和形狀; 然後影響下一段所需的位置。

現代瀏覽器探祕(part3):渲染
圖6:由於換行符而移動的段落的框佈局

CSS可以使元素浮動到一側,掩蓋溢位項,並更改寫入方向。 你可以想象,這個佈局階段是一項艱鉅的任務。 在Chrome專案中,有一個完整的工程師團隊負責佈局。 如果你想看到他們工作的細節,看看這些會議記錄非常有意思。

繪製

擁有了DOM、樣式和佈局仍然不足以呈現頁面。 假設你正在嘗試重現一幅畫。 你不僅需知道元素的大小,形狀和位置,還需要判斷繪製它們的順序。

圖7:一個在畫布前拿著畫筆的人,正在思考是應該先畫圓圈還是矩形

圖7:一個在畫布前拿著畫筆的人,正在思考是應該先畫圓圈還是矩形

例如:可以為某些元素設定z-index,在這種情況下,按HTML中編寫的元素順序繪製將導致不正確的呈現。

圖8:頁面元素按HTML標記的順序出現,會導致錯誤的渲染影像,因為沒有考慮z-index

圖8:頁面元素按HTML標記的順序出現,會導致錯誤的渲染影像,因為沒有考慮z-index

在此繪製步驟中,主執行緒遍歷佈局樹以建立繪製記錄。 繪製記錄是繪製過程的一個註釋,如“背景優先,然後是文字,最後是矩形”。 如果你使用JavaScript繪製了<canvas>元素,那麼可能對此過程很熟悉。

圖9:主執行緒遍歷佈局樹並生成繪製記錄

圖9:主執行緒遍歷佈局樹並生成繪製記錄

更新渲染通道的成本很高

在渲染通道中最重要的一件事就是在每個步驟中,前一個操作的結果被用於建立新資料。 例如:如果佈局樹中的某些內容發生更改,則需要為文件的受影響部分重新生成繪製順序。

現代瀏覽器探祕(part3):渲染
圖10:DOM + Style,佈局和繪製樹的生成順序

如果要為元素設定動畫,則瀏覽器必須在每個幀之間執行這些操作。 我們的大多數顯示器每秒重新整理螢幕60次(60 fps); 當你在每一幀移動螢幕時,動畫對人眼來說會很平滑。 但是如果動畫錯過了其中的幀,則頁面將發生閃爍。

圖11:時間軸上的動畫幀

圖11:時間軸上的動畫幀

即使你的渲染操作能夠跟上螢幕重新整理,這些計算也是在主執行緒上執行的,這意味著當你的應用執行 JavaScript 時它可能會被阻止。

圖12:時間軸上的動畫幀,但JavaScript阻止了一幀

圖12:時間軸上的動畫幀,但JavaScript阻止了一幀

你可以將JavaScript操作劃分為小塊,並使用 requestAnimationFrame() 安排在每個幀上執行。 有關此主題的更多資訊,請參閱優化JavaScript執行。 你也可以在 Web Workers 中執行 JavaScript 來避免阻塞主執行緒

圖13:在動畫幀的時間軸上執行的較小的JavaScript塊

圖13:在動畫幀的時間軸上執行的較小的JavaScript塊

合成

你會如何繪製一個頁面?

現在瀏覽器知道文件的結構,每個元素的樣式,頁面的幾何形狀和繪製順序,它是如何繪製頁面的? 將此資訊轉換為螢幕上的畫素稱為光柵化。

現代瀏覽器探祕(part3):渲染

圖14:簡單光柵化過程

也許處理這種情況的一種簡單的方法是在視口(viewport)內部使用柵格部件。 如果使用者滾動頁面,則移動光柵幀,並通過更多光柵填充缺少的部分。 這就是Chrome首次釋出時處理柵格化的方式。 但是,現代瀏覽器執行一個稱為合成的更復雜的過程。

什麼是合成

合成是一種將頁面的各個部分分層,分別柵格化,並在一個被稱為合成器執行緒的獨立執行緒中合成為頁面的技術。 如果發生滾動,由於圖層已經被柵格化,所以它所要做的就是合成一個新幀。 通過移動圖層和合成新幀,可以用相同的方式實現動畫。

現代瀏覽器探祕(part3):渲染
圖15:合成過程的示意動畫

你可以使用瀏覽器開發者工具的“layout”皮膚中檢視你的網站如何劃分為多個圖層

分為幾層

為了找出哪些元素需要放在哪些層中,主執行緒通過遍歷佈局樹以建立層樹(此部分在DevTools效能皮膚中稱為“Update Layer Tree”)。 如果頁面某些應該是單獨圖層(如滑入式側面選單)的部分但是沒有分配到圖層,那麼你可以使用CSS中的will-change屬性提示瀏覽器。

圖16:主執行緒生通過遍歷佈局樹來成層樹

圖16:主執行緒生通過遍歷佈局樹來成層樹

也許你想要為每個元素提供圖層,但是過多的圖層進行合成可能會導致比每幀光柵化頁面的小部分更慢的操作,因此測量應用程式的渲染效能至關重要。 有關主題的更多資訊,請參閱Stick to Compositor-Only Properties and Manage Layer Count

光柵和複合關閉主執行緒

一旦建立了層樹並確定了繪製順序,主執行緒就會將該資訊提交給合成器執行緒。 合成器執行緒然後柵格化每個圖層。 一個圖層可能像頁面的整個長度一樣大,因此合成器執行緒會將它們分成圖塊,並將每個圖塊傳送到光柵執行緒。 柵格執行緒柵格化每一個tile並將它們儲存在GPU記憶體中。

圖17:柵格執行緒建立tile點陣圖併傳送到GPU

圖17:柵格執行緒建立tile點陣圖併傳送到GPU

合成器執行緒可以優先考慮不同的aster執行緒,以便視口(或附近)內的事物可以先被光柵化。 圖層還具有多個不同解析度的傾斜度,可以處理放大操作等內容。

一旦tile被光柵化,合成器執行緒會收集稱為繪製四邊形(draw quads )的tile資訊來建立合成器幀(compositor frame)

繪製四邊形 包含資訊,例如圖塊在記憶體中的位置以及在考慮頁面合成的情況下繪製圖塊的頁面中的位置。
合成器幀 表示頁面幀的繪製四邊形的集合。

然後通過IPC將合成器幀提交給瀏覽器程式。這時可以從UI執行緒新增另一個合成器幀以用於瀏覽器UI更改,或者從其他渲染器程式新增擴充資料。 這些合成器幀被髮送到GPU用來在螢幕上顯示。 如果發生滾動事件,合成器執行緒會建立另一個合成器幀併傳送到GPU。

圖18:合成器執行緒建立合成幀。 幀先被髮送到瀏覽器程式,然後再傳送到GPU

圖18:合成器執行緒建立合成幀。 幀先被髮送到瀏覽器程式,然後再傳送到GPU

合成的好處是它可以在不涉及主執行緒的情況下完成。 合成執行緒不需要等待樣式計算或 JavaScript 執行。 這就是合成動畫是平滑效能的最佳選擇的原因。 如果需要再次計算佈局或繪圖,則必須涉及主執行緒。

總結

在本文中,我們研究了從解析到合成的渲染通道。

在本系列的下一篇文章中,我們將更詳細地介紹合成器執行緒,並瞭解當使用者進行滑鼠移動和單擊等操作時會發生什麼。

原文首發於京程一燈公眾號:jingchengyideng

相關文章