深入瞭解新式網路瀏覽器 (第 3 部分)

Mariko Kosaka

轉譯器程序的內部運作方式

這是部落格系列文章的第 3 篇,共 4 篇,探討瀏覽器的運作方式。我們先前已介紹多程序架構和導覽流程。在本篇文章中,我們將探討轉譯器程序內發生的情況。

轉譯器程序會影響許多網頁效能層面。由於轉譯器處理程序內發生的事件很多,因此本篇文章僅提供概略的介紹。如要進一步瞭解,請參閱 Web 基礎知識的「成效」部分,這裡有更多資源。

轉譯器處理程序會處理網頁內容

算繪程序負責處理分頁中發生的所有事件。在轉譯器處理程序中,主執行緒會處理您傳送給使用者的大部分程式碼。如果您使用 Web Worker 或 Service Worker,部分 JavaScript 可能會由 worker 執行緒處理。合成器和光柵執行緒也會在轉譯器程序中執行,以便順利且有效率地轉譯網頁。

轉譯器程序的主要工作,就是將 HTML、CSS 和 JavaScript 轉換成使用者可互動的網頁。

轉譯器程序
圖 1:轉譯器程序,其中包含主執行緒、工作執行緒、轉譯器執行緒和內部的光柵執行緒

剖析

DOM 的建構

當轉譯器程序收到導覽的提交訊息,並開始接收 HTML 資料時,主執行緒就會開始剖析文字字串 (HTML),並將其轉換為 Document Object Model (DOM)。

DOM 是瀏覽器對網頁的內部表示法,也是網頁開發人員可透過 JavaScript 互動的資料結構和 API。

將 HTML 文件剖析成 DOM 的作業是由 HTML 標準定義。您可能已經注意到,向瀏覽器提供 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 規格中的「An introduction to error handling and strange cases in the parser」一節。

子資源載入

網站通常會使用圖片、CSS 和 JavaScript 等外部資源。這些檔案必須從網路或快取載入。主執行緒「可以」在剖析時逐一要求這些元素,以便建構 DOM,但為了加快速度,「預先載入掃描器」會並行執行。如果 HTML 文件中含有 <img> 或 <link> 之類的內容,預先載入掃描器會查看 HTML 剖析器產生的符記,並將要求傳送至瀏覽器程序中的網路執行緒。

DOM
圖 2:主執行緒剖析 HTML 並建構 DOM 樹狀結構

JavaScript 可以封鎖剖析作業

HTML 剖析器找到 <script> 標記時,會暫停剖析 HTML 文件,並必須載入、剖析及執行 JavaScript 程式碼。原因是 JavaScript 可以使用 document.write() 之類的項目變更文件形狀,而 document.write() 會變更整個 DOM 結構 (HTML 規格中的剖析模型總覽有個不錯的圖表)。因此,HTML 剖析器必須等待 JavaScript 執行完畢,才能繼續剖析 HTML 文件。如果您想知道 JavaScript 執行程序會發生什麼事,請參閱 V8 團隊的演講和網誌文章。

向瀏覽器提供您想要載入資源的方式提示

網頁開發人員可以透過多種方式向瀏覽器傳送提示,以便順利載入資源。如果 JavaScript 未使用 document.write(),您可以將 async 或 defer 屬性新增至 <script> 標記。接著,瀏覽器會以非同步方式載入及執行 JavaScript 程式碼,且不會阻擋剖析作業。您也可以使用JavaScript 模組 (如果適用的話)。<link rel="preload"> 可用於通知瀏覽器,目前導覽作業確實需要這項資源,且您希望盡快下載。如要進一步瞭解這項功能,請參閱「資源優先順序 – 讓瀏覽器協助您」。

樣式計算

我們可以在 CSS 中為網頁元素設定樣式,因此光是使用 DOM 就不足以瞭解網頁的外觀。主執行緒會剖析 CSS,並判斷每個 DOM 節點的運算樣式。這項資訊是關於根據 CSS 選取器,將哪種樣式套用至每個元素。您可以在開發人員工具的 computed 專區中查看這項資訊。

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

即使您未提供任何 CSS,每個 DOM 節點都會有計算樣式。<h1> 標記會顯示比 <h2> 標記大,且系統會為每個元素定義邊界。這是因為瀏覽器有預設的樣式表單。如要瞭解 Chrome 的預設 CSS,請參閱這裡的原始碼。

版面配置

此時,轉譯器程序已知悉文件的結構和每個節點的樣式,但這還不足以轉譯網頁。假設您想透過手機向朋友描述一幅畫作。「畫面上有一個大紅圈和一個小藍方塊」這類資訊不足以讓朋友瞭解畫作實際外觀。

人類傳真機遊戲
圖 4:某人站在一幅畫前,電話線連線至另一個人

版面配置是尋找元素幾何圖形的程序。主執行緒會逐一檢查 DOM 和計算樣式,並建立版面配置樹狀結構,其中包含 x 和 y 座標和邊界框大小等資訊。版面配置樹狀結構的結構可能與 DOM 樹狀結構相似,但只包含與網頁上顯示內容相關的資訊。如果套用 display: none,該元素就不會是版面配置樹狀結構的一部分 (不過,帶有 visibility: hidden 的元素會位於版面配置樹狀結構中)。同樣地,如果套用含有 p::before{content:"Hi!"} 等內容的擬似元素,即使該元素不在 DOM 中,也會納入版面配置樹狀結構。

版面配置
圖 5:主執行緒會處理 DOM 樹狀結構,並產生計算式樣式和版面配置樹狀結構
圖 6:因換行符號變更而移動的段落方塊版面配置

決定網頁的版面配置是一項艱鉅的任務。即使是最簡單的頁面版面配置,例如從上到下的區塊流程,也必須考量字型大小和換行位置,因為這些因素會影響段落的大小和形狀,進而影響後續段落的位置。

CSS 可讓元素浮動到某一側、遮蔽溢位項目,以及變更書寫方向。您可以想像,這個版面配置階段有著艱鉅的任務。在 Chrome 中,整個工程師團隊都會著手處理版面配置。如要查看他們的工作詳細資料,請觀看 BlinkOn 大會的幾場演講錄影,內容相當有趣。

油漆

繪圖遊戲
圖 7:某人站在畫布前方,手持畫筆,正在考慮應先畫圓還是正方形

有了 DOM、樣式和版面配置,仍不足以轉譯網頁。假設您要複製一幅畫作,您知道元素的大小、形狀和位置,但仍須判斷繪製順序。

舉例來說,z-index 可能會針對特定元素進行設定,在這種情況下,依 HTML 中所寫元素的順序進行繪製,會導致算繪不正確。

z-index 失敗
圖 8:網頁元素依 HTML 標記順序顯示,但未考量 z-index,因此產生錯誤的算繪圖片

在這個繪製步驟中,主執行緒會逐一檢查版面配置樹狀結構,以建立繪製記錄。繪圖記錄是繪圖程序的備註,例如「先繪製背景、再繪製文字、再繪製矩形」。如果您曾使用 JavaScript 在 <canvas> 元素上繪製圖形,這個程序可能會讓您感到熟悉。

繪製記錄
圖 9:主執行緒會逐一檢視版面配置樹狀結構,並產生繪圖記錄

更新算繪管道成本高昂

圖 10:DOM+Style、Layout 和 Paint 樹狀結構的產生順序

在轉譯管道中,最重要的是瞭解在每個步驟中,前一個作業的結果會用於建立新資料。舉例來說,如果版面配置樹狀結構發生變更,則需要針對文件的受影響部分重新產生繪圖順序。

如果您要為元素製作動畫,瀏覽器必須在每個影格之間執行這些作業。我們的大部分螢幕每秒會刷新 60 次 (60 fps);如果您在每個影格中在畫面上移動物件,動畫就會以流暢的速度呈現。不過,如果動畫錯過中間的畫格,網頁就會看起來「不自然」。

因缺少影格而導致的卡頓
圖 11:時間軸上的動畫影格

即使算繪作業能跟上螢幕刷新作業,這些計算仍會在主執行緒上執行,這表示在應用程式執行 JavaScript 時,可能會遭到封鎖。

由 JavaScript 造成的 jank
圖 12:時間軸上的動畫影格,但其中一個影格遭 JavaScript 封鎖

您可以將 JavaScript 作業分割成小片段,並使用 requestAnimationFrame() 排程,以便在每個影格執行。如要進一步瞭解這個主題,請參閱「最佳化 JavaScript 執行作業」。您也可以在 Web Workers 中執行 JavaScript,避免封鎖主執行緒。

要求動畫頁框
圖 13:在時間軸上搭配動畫影格執行的較小 JavaScript 區塊

合成

你會如何繪製網頁?

圖 14:簡單的算繪程序動畫

瀏覽器現在已知文件結構、每個元素的樣式、網頁的幾何圖形和繪製順序,那麼它會如何繪製網頁?將這項資訊轉換為螢幕上的像素稱為「轉柵」。

處理這種情況的簡單方法,或許是將可視區域內的部分內容轉為點陣圖。如果使用者捲動頁面,則會移動已經過掃描的框架,並透過更多掃描作業填入缺少的部分。這是 Chrome 在首次發布時處理算繪的方式。不過,現代瀏覽器會執行更複雜的程序,稱為合成。

什麼是合成

圖 15:合成程序的動畫

合成是一種將網頁的部分內容分割成多個圖層,並分別將其轉為點陣圖,然後在稱為合成器執行緒的個別執行緒中合成為網頁的技術。如果發生捲動,由於圖層已經過算繪,因此只需合成新影格即可。動畫的製作方式也相同,只要移動圖層並合成新影格即可。

您可以使用「Layers」面板,查看網站如何在開發人員工具中劃分為多個圖層。

將圖層分割

為了找出哪些元素需要位於哪些圖層,主執行緒會逐一檢查版面配置樹狀結構,以建立圖層樹狀結構 (這個部分在 DevTools 效能面板中稱為「Update Layer Tree」)。如果網頁的某些部分應為獨立層級 (例如滑入式側邊選單),但並未獲得獨立層級,您可以在 CSS 中使用 will-change 屬性,向瀏覽器提供提示。

圖層樹狀結構
圖 16:主要執行緒會逐一檢視版面配置樹狀結構,產生圖層樹狀結構

您可能會想為每個元素提供圖層,但在過多圖層上進行合成,可能會導致操作速度變慢,比起在每個影格中將網頁的某些小部分轉為點陣圖,更不利於效能,因此評估應用程式的算繪效能至關重要。如要進一步瞭解這個主題,請參閱「只使用合成器專屬屬性和管理圖層數量」。

將光柵和合成作業移出主執行緒

建立圖層樹狀結構並決定繪製順序後,主執行緒會將該資訊提交至合成器執行緒。接著,合成器執行緒會將每個圖層轉為點陣圖。圖層可能很大,例如整個網頁的長度,因此合成器執行緒會將圖層分割成圖塊,並將每個圖塊傳送至光柵執行緒。光柵執行緒會將每個圖塊轉為光柵,並儲存在 GPU 記憶體中。

光柵
圖 17:光柵執行緒建立圖塊的點陣圖並傳送至 GPU

合成器執行緒可以將優先順序給予不同的轉譯執行緒,以便先轉譯檢視區 (或附近) 中的項目。圖層也有多個不同解析度的平鋪,用於處理放大動作等事項。

將圖塊轉為點陣圖後,合成器執行緒會收集稱為「繪製四邊形」的圖塊資訊,以建立合成器影格。

繪製四邊形 包含資訊,例如圖塊在記憶體中的所在位置,以及考量網頁合成作業後,在網頁中繪製圖塊的位置。
合成器影格 代表網頁影格的繪圖四邊形集合。

接著,系統會透過 IPC 將合成器影格提交至瀏覽器程序。此時,您可以從 UI 執行緒新增另一個合成器影格,以便瀏覽器 UI 變更,或從其他轉譯器程序新增片段。這些合成器影格會傳送至 GPU,以便在螢幕上顯示。如果捲動事件傳入,合成器執行緒會建立另一個要傳送至 GPU 的合成器影格。

composit
圖 18:合成器執行緒建立合成影格。影格會先傳送至瀏覽器程序,再傳送至 GPU

合成作業的好處是,它不會涉及主執行緒。合成器執行緒不需要等待樣式計算或 JavaScript 執行作業。因此,只合成動畫可提供最佳的流暢效能。如果需要再次計算版面配置或繪製作業,則必須使用主執行緒。

總結

在本篇文章中,我們探討了從剖析到合成的轉譯管道。希望您現在能進一步瞭解如何改善網站效能。

在本系列的下一篇也是最後一篇文章中,我們將進一步探討合成器執行緒,並瞭解 mouse move 和 click 等使用者輸入內容傳入時會發生什麼事。

你喜歡這篇文章嗎?如有任何問題或建議,歡迎在下方的留言區留言,或在 Twitter 上傳送訊息給 @kosamari。

接下來:輸入內容會傳送至合成器