頁面填充:瀏覽器的工作原理

使用者希望 Web 體驗能夠快速載入內容並流暢互動。因此,開發人員應努力實現這兩個目標。

要了解如何改進效能和感知效能,有助於瞭解瀏覽器的工作原理。

概述

快速的網站提供更好的使用者體驗。使用者希望並期望 Web 體驗能夠快速載入內容並流暢互動。

Web 效能中的兩個主要問題是與延遲相關的問題以及與瀏覽器在大多數情況下是單執行緒相關的問題。

延遲是我們確保頁面快速載入能力的最大威脅。開發人員的目標是使網站儘可能快地載入——或者至少 *看起來* 載入得非常快——以便使用者能夠儘快獲取所需的資訊。網路延遲是指透過空中傳輸位元組到計算機所需的時間。Web 效能是我們必須做的事情,以使頁面儘可能快地載入。

在大多數情況下,瀏覽器被認為是單執行緒的。也就是說,它們在執行另一個任務之前會從頭到尾執行一個任務。為了實現流暢的互動,開發人員的目標是確保網站互動的效能,從流暢的滾動到對觸控的響應。渲染時間是關鍵,確保主執行緒可以完成我們賦予它的所有工作,並且始終可以處理使用者互動。通過了解瀏覽器的單執行緒特性並儘可能地減少主執行緒的職責,可以提高 Web 效能,從而確保渲染流暢,對互動的響應即時。

導航是載入網頁的第一步。它發生在使用者透過在位址列中輸入 URL、點選連結、提交表單以及其他操作請求頁面時。

Web 效能的目標之一是最小化導航完成所需的時間。在理想條件下,這通常不會花費太長時間,但延遲和頻寬是可能導致延遲的敵人。

DNS 查詢

導航到網頁的第一步是找到該頁面的資源所在的位置。如果您導航到 https://example.com,則 HTML 頁面位於 IP 地址為 93.184.216.34 的伺服器上。如果您從未訪問過此網站,則必須進行 DNS 查詢。

您的瀏覽器請求 DNS 查詢,該查詢最終由名稱伺服器處理,然後名稱伺服器會響應一個 IP 地址。在此初始請求之後,IP 可能會被快取一段時間,這可以透過從快取中檢索 IP 地址而不是再次聯絡名稱伺服器來加快後續請求的速度。

DNS 查詢通常每個主機名只需要執行一次頁面載入。但是,必須為請求的頁面引用的每個唯一主機名執行 DNS 查詢。如果您的字型、影像、指令碼、廣告和指標都具有不同的主機名,則必須為每個主機名進行 DNS 查詢。

Mobile requests go first to the cell tower, then to a central phone company computer before being sent to the internet

這可能會對效能造成問題,尤其是在行動網路上。當用戶使用行動網路時,每個 DNS 查詢都必須從手機到蜂窩塔才能到達權威 DNS 伺服器。手機、蜂窩塔和名稱伺服器之間的距離可能會增加很大的延遲。

TCP 握手

一旦知道 IP 地址,瀏覽器就會透過 TCP 三次握手 建立與伺服器的連線。這種機制的設計是為了讓兩個試圖通訊的實體——在本例中是瀏覽器和 Web 伺服器——在傳輸資料之前協商網路 TCP 套接字連線的引數,通常透過 HTTPS 進行。

TCP 的三次握手技術通常稱為“SYN-SYN-ACK”——或更準確地說是 SYN、SYN-ACK、ACK——因為 TCP 傳輸了三個訊息來協商並在兩臺計算機之間啟動 TCP 會話。是的,這意味著在每個伺服器之間來回傳輸三個訊息,並且請求尚未發出。

TLS 協商

對於透過 HTTPS 建立的安全連線,需要另一個“握手”。這個握手,或者更準確地說,TLS 協商,確定將使用哪個密碼來加密通訊,驗證伺服器,並在開始實際資料傳輸之前建立安全連線。這需要在實際傳送內容請求之前再進行五次往返伺服器。

The DNS lookup, the TCP handshake, and 5 steps of the TLS handshake including clienthello, serverhello and certificate, clientkey and finished for both server and client.

雖然建立安全連線會增加頁面載入時間,但安全連線值得延遲開銷,因為瀏覽器和 Web 伺服器之間傳輸的資料不會被第三方解密。

在進行了八次往返伺服器之後,瀏覽器終於能夠發出請求了。

響應

一旦我們與 Web 伺服器建立了連線,瀏覽器就會代表使用者傳送初始 HTTP GET 請求,對於網站來說,這通常是一個 HTML 檔案。伺服器收到請求後,將回複相關的響應頭和 HTML 內容。

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>My simple page</title>
    <link rel="stylesheet" href="styles.css" />
    <script src="myscript.js"></script>
  </head>
  <body>
    <h1 class="heading">My Page</h1>
    <p>A paragraph with a <a href="https://example.com/about">link</a></p>
    <div>
      <img src="myimage.jpg" alt="image description" />
    </div>
    <script src="anotherscript.js"></script>
  </body>
</html>

此初始請求的響應包含接收到的第一個資料位元組。第一個位元組時間 (TTFB) 是使用者發出請求(例如,透過點選連結)到收到此第一個 HTML 資料包之間的時間。第一塊內容通常是 14KB 的資料。

在我們上面的示例中,請求肯定小於 14KB,但連結的資源直到瀏覽器在解析過程中遇到連結時才會被請求,如下所述。

擁塞控制 / TCP 慢啟動

TCP 資料包在傳輸過程中被分割成段。由於 TCP 保證資料包的順序,因此伺服器必須在傳送一定數量的段後,從客戶端以 ACK 資料包的形式接收確認。

如果伺服器在每個段之後等待 ACK,這將導致客戶端頻繁傳送 ACK,並可能增加傳輸時間,即使是在網路負載較低的情況下。

另一方面,一次傳送太多段會導致一個問題,即在繁忙的網路中,客戶端將無法接收段,並且只會長時間響應 ACK,伺服器將不得不繼續重新發送段。

為了平衡傳輸段的數量,使用 TCP 慢啟動 演算法逐漸增加傳輸資料量,直到確定最大網路頻寬,並在網路負載較高時減少傳輸資料量。

要傳輸的段數由擁塞視窗 (CWND) 的值控制,該值可以初始化為 1、2、4 或 10 MSS(MSS 在乙太網協議上為 1500 位元組)。該值是要傳送的位元組數,在接收到該值後,客戶端必須傳送 ACK。

如果收到 ACK,則 CWND 值將加倍,因此伺服器下次可以傳送更多段。如果未收到 ACK,則 CWND 值將減半。因此,該機制在傳送過多段和傳送過少段之間取得了平衡。

解析

瀏覽器收到第一塊資料後,就可以開始解析接收到的資訊了。解析 是瀏覽器將透過網路接收到的資料轉換為 DOMCSSOM 的步驟,渲染器使用它將頁面繪製到螢幕上。

DOM 是瀏覽器對標記的內部表示。DOM 也可以透過 JavaScript 中的各種 API 公開和操作。

即使請求的頁面的 HTML 大於初始的 14KB 資料包,瀏覽器也會開始解析並嘗試基於它擁有的資料渲染體驗。這就是為什麼 Web 效能最佳化需要包含瀏覽器開始渲染頁面所需的一切,或者至少是頁面的模板——第一次渲染所需的 CSS 和 HTML——在第一個 14KB 中。但在將任何內容渲染到螢幕之前,必須解析 HTML、CSS 和 JavaScript。

構建 DOM 樹

我們在 關鍵渲染路徑 中描述了五個步驟。

第一步是處理 HTML 標記並構建 DOM 樹。HTML 解析涉及標記化和樹構建。HTML 標記包括開始和結束標籤,以及屬性名稱和值。如果文件格式良好,則解析它很簡單且更快。解析器將標記化的輸入解析到文件中,構建文件樹。

DOM 樹描述了文件的內容。<html> 元素是文件樹的第一個元素和根節點。該樹反映了不同元素之間的關係和層次結構。巢狀在其他元素中的元素是子節點。DOM 節點數越多,構建 DOM 樹所需的時間就越長。

The DOM tree for our sample code, showing all the nodes, including text nodes.

當解析器找到非阻塞資源(例如影像)時,瀏覽器將請求這些資源並繼續解析。遇到 CSS 檔案時可以繼續解析,但<script> 元素(特別是那些沒有asyncdefer 屬性的元素)會阻止渲染並暫停 HTML 的解析。儘管瀏覽器的預載入掃描器加快了此過程,但過多的指令碼仍然可能是一個重大的瓶頸。

預載入掃描器

在瀏覽器構建 DOM 樹的同時,此過程會佔用主執行緒。在此過程中,預載入掃描器將解析可用的內容並請求高優先順序資源,例如 CSS、JavaScript 和網路字型。由於預載入掃描器,我們不必等到解析器找到對外部資源的引用才能請求它。它將在後臺檢索資源,以便當主 HTML 解析器到達請求的資產時,它們可能已經在傳輸中或已下載。預載入掃描器提供的最佳化減少了阻塞。

html
<link rel="stylesheet" href="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description" />
<script src="anotherscript.js" async></script>

在此示例中,當主執行緒解析 HTML 和 CSS 時,預載入掃描器將找到指令碼和影像,並開始下載它們。為了確保指令碼不會阻塞該過程,請新增async 屬性,或者如果 JavaScript 解析和執行順序很重要,則新增defer 屬性。

等待獲取 CSS 不會阻塞 HTML 解析或下載,但會阻塞 JavaScript,因為 JavaScript 通常用於查詢 CSS 屬性對元素的影響。

構建 CSSOM 樹

關鍵渲染路徑的第二步是處理 CSS 並構建 CSSOM 樹。CSS 物件模型類似於 DOM。DOM 和 CSSOM 都是樹。它們是獨立的資料結構。瀏覽器將 CSS 規則轉換為它可以理解和使用的樣式對映。瀏覽器遍歷 CSS 中的每個規則集,根據 CSS 選擇器建立具有父子和兄弟關係的節點樹。

與 HTML 一樣,瀏覽器需要將接收到的 CSS 規則轉換為它可以使用的內容。因此,它重複了 HTML 到物件的處理過程,但用於 CSS。

CSSOM 樹包含來自使用者代理樣式表的樣式。瀏覽器從適用於節點的最通用規則開始,並透過應用更具體的規則遞迴地細化計算出的樣式。換句話說,它級聯屬性值。

構建 CSSOM 非常快,並且在當前的開發者工具中沒有以獨特的顏色顯示。相反,開發者工具中的“重新計算樣式”顯示瞭解析 CSS、構建 CSSOM 樹和遞迴計算計算樣式所需的時間。在 Web 效能最佳化方面,有一些更簡單的成果,因為建立 CSSOM 的總時間通常小於進行一次 DNS 查詢所需的時間。

其他程序

JavaScript 編譯

在 CSS 被解析和 CSSOM 被建立的同時,其他資產(包括 JavaScript 檔案)正在下載(感謝預載入掃描器)。JavaScript 被解析、編譯和解釋。指令碼被解析成抽象語法樹。一些瀏覽器引擎獲取抽象語法樹 並將其傳遞給編譯器,輸出位元組碼。這稱為 JavaScript 編譯。大部分程式碼是在主執行緒上解釋的,但也有例外,例如在Web Worker中執行的程式碼。

構建可訪問性樹

瀏覽器還會構建一個可訪問性樹,輔助裝置使用該樹來解析和解釋內容。可訪問性物件模型 (AOM) 就像 DOM 的語義版本。DOM 更新時,瀏覽器會更新可訪問性樹。輔助技術本身無法修改可訪問性樹。

在構建 AOM 之前,內容無法被螢幕閱讀器訪問。

渲染

渲染步驟包括樣式、佈局、繪製以及在某些情況下進行合成。在解析步驟中建立的 CSSOM 和 DOM 樹組合成渲染樹,然後使用該樹計算每個可見元素的佈局,然後將其繪製到螢幕上。在某些情況下,內容可以提升到自己的圖層並進行合成,透過在 GPU 而不是 CPU 上的執行執行緒上繪製螢幕的部分內容來提高效能,從而釋放主執行緒。

樣式

關鍵渲染路徑的第三步是將 DOM 和 CSSOM 組合成渲染樹。計算出的樣式樹或渲染樹的構建從 DOM 樹的根開始,遍歷每個可見節點。

不會顯示的元素,例如<head> 元素及其子元素以及任何具有display: none 的節點(例如,您會在使用者代理樣式表中找到的script { display: none; })不包含在渲染樹中,因為它們不會出現在渲染輸出中。應用了visibility: hidden 的節點包含在渲染樹中,因為它們確實佔用空間。由於我們沒有給出任何指令來覆蓋使用者代理預設值,因此我們上面程式碼示例中的script 節點將不包含在渲染樹中。

每個可見節點都應用了其 CSSOM 規則。渲染樹儲存所有具有內容和計算樣式的可見節點——將所有相關樣式與 DOM 樹中的每個可見節點匹配,並根據CSS 級聯確定每個節點的計算樣式。

佈局

關鍵渲染路徑的第四步是在渲染樹上執行佈局以計算每個節點的幾何形狀。佈局是確定渲染樹中所有節點的尺寸和位置的過程,以及確定頁面上每個物件的大小和位置。重排是隨後對頁面或整個文件的任何部分進行的任何尺寸和位置確定。

構建渲染樹後,佈局開始。渲染樹識別哪些節點顯示(即使不可見)以及它們的計算樣式,但沒有識別每個節點的尺寸或位置。為了確定每個物件的確切大小和位置,瀏覽器從渲染樹的根開始並遍歷它。

在網頁上,幾乎所有東西都是一個盒子。不同的裝置和不同的桌面首選項意味著無限數量的不同視口大小。在此階段,考慮到視口大小,瀏覽器確定螢幕上所有不同框的大小。以視口大小為基礎,佈局通常從主體開始,佈局主體所有後代的大小,每個元素的盒子模型屬性為替換元素提供佔位符空間,它不知道這些元素的尺寸,例如我們的影像。

第一次確定每個節點的大小和位置稱為佈局。隨後的重新計算稱為重排。在我們的示例中,假設初始佈局發生在影像返回之前。由於我們沒有宣告影像的尺寸,因此一旦知道影像尺寸,就會發生重排。

繪製

關鍵渲染路徑的最後一步是將各個節點繪製到螢幕上,第一次出現稱為首次內容繪製。在繪製或光柵化階段,瀏覽器將佈局階段中計算的每個框轉換為螢幕上的實際畫素。繪製涉及將元素的每個視覺部分繪製到螢幕上,包括文字、顏色、邊框、陰影和替換元素(如按鈕和影像)。瀏覽器需要非常快地執行此操作。

為了確保平滑滾動和動畫,佔用主執行緒的所有內容,包括計算樣式以及重排和繪製,都必須在瀏覽器中花費不到 16.67 毫秒才能完成。在 2048 x 1536 時,iPad 有超過 3,145,000 個畫素需要繪製到螢幕上。需要非常快地繪製大量畫素。為了確保重新繪製的速度甚至比初始繪製更快,螢幕繪製通常分解成幾個圖層。如果發生這種情況,則需要合成。

繪製可以將佈局樹中的元素分解成圖層。將內容提升到 GPU 上的圖層(而不是 CPU 上的主執行緒)可以提高繪製和重新繪製效能。有一些特定的屬性和元素會例項化圖層,包括<video><canvas>,以及任何具有 CSS 屬性opacity、3D transformwill-change 和其他一些屬性的元素。這些節點將繪製到它們自己的圖層上,以及它們的後代,除非後代由於上述一個(或多個)原因需要它自己的圖層。

圖層確實可以提高效能,但在記憶體管理方面成本很高,因此不應在 Web 效能最佳化策略中過度使用。

合成

當文件的各個部分在不同的圖層中繪製,相互重疊時,需要進行合成以確保它們按正確的順序繪製到螢幕上,並且內容正確呈現。

隨著頁面繼續載入資產,可能會發生重排(回想一下我們延遲到達的示例影像)。重排會引發重新繪製和重新合成。如果我們定義了影像的尺寸,則無需重排,並且只需要重新繪製需要重新繪製的圖層,並在必要時進行合成。但我們沒有包含影像尺寸!從伺服器獲取影像後,渲染過程會返回到佈局步驟並從那裡重新開始。

互動性

一旦主執行緒完成頁面繪製,您可能會認為我們“一切就緒”。情況並非總是如此。如果載入包括 JavaScript,並且已正確延遲,並且僅在onload 事件觸發後執行,則主執行緒可能很忙,並且無法用於滾動、觸控和其他互動。

頁面互動時間 (TTI) 是衡量從初始請求(導致 DNS 解析和 TCP 連線)到頁面可互動之間的時間。可互動是指在首次內容繪製之後,頁面在 50 毫秒內響應使用者互動的時間點。如果主執行緒忙於解析、編譯和執行 JavaScript,則它不可用,因此無法及時(少於 50 毫秒)響應使用者互動。

例如,影像可能載入很快,但 anotherscript.js 檔案可能為 2MB,並且使用者的網路連線速度很慢。在這種情況下,使用者會很快看到頁面,但在指令碼下載、解析和執行完成之前,無法流暢滾動。這並非良好的使用者體驗。避免佔用主執行緒,如 WebPageTest 示例所示。

The main thread is occupied by the downloading, parsing and execution of a JavaScript file - over a fast connection

在本例中,JavaScript 執行花費了超過 1.5 秒,並且主執行緒在此期間完全被佔用,無法響應點選事件或螢幕點選。

另請參閱