填充頁面:瀏覽器如何工作

使用者希望獲得載入速度快、互動流暢的網頁體驗。因此,開發者應努力實現這兩個目標。

要了解如何提高效能和感知效能,瞭解瀏覽器的工作原理會有所幫助。

概述

快速的網站提供更好的使用者體驗。使用者希望並期待內容載入速度快、互動流暢的網頁體驗。

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

延遲是我們確保頁面快速載入能力的最大威脅。開發者的目標是使網站儘可能快地載入——或者至少看起來載入得非常快——這樣使用者就能儘快獲得所請求的資訊。網路延遲是將位元組透過空中傳輸到計算機所需的時間。網頁效能是我們為使頁面儘快載入所必須做的工作。

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

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

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

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 client hello, server hello and certificate, client key and finished for both server and client.

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

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

Response

一旦我們與 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="my-image.jpg" alt="image description" />
    </div>
    <script src="another-script.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 資料包,瀏覽器也會開始解析並嘗試根據其擁有的資料渲染體驗。這就是為什麼網頁效能最佳化必須在最初的 14KB 中包含瀏覽器開始渲染頁面所需的一切,或者至少是頁面模板——第一次渲染所需的 CSS 和 HTML。但在螢幕上渲染任何內容之前,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 和 Web 字型。多虧了預載入掃描器,我們不必等到解析器找到對外部資源的引用才請求它。它會在後臺檢索資源,這樣當主 HTML 解析器到達請求的資產時,它們可能已經在傳輸中或已經下載。預載入掃描器提供的最佳化減少了阻塞。

html
<link rel="stylesheet" href="styles.css" />
<script src="my-script.js" async></script>
<img src="my-image.jpg" alt="image description" />
<script src="another-script.js" async></script>

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

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

構建 CSSOM 樹

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

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

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

構建 CSSOM 非常非常快,此構建時間資訊不會在開發人員工具中顯示。相反,開發人員工具中的“重新計算樣式”顯示解析 CSS、構建 CSSOM 樹和遞迴計算計算樣式的總時間。在網頁效能方面,有許多更好的方法來投入最佳化工作,因為建立 CSSOM 的總時間通常小於一次 DNS 查詢所需的時間。

其他程序

JavaScript 編譯

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

構建可訪問性樹

瀏覽器還會構建一個可訪問性樹,輔助裝置使用它來解析和解釋內容。可訪問性物件模型(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>,以及任何具有opacity、3D transformwill-change等 CSS 屬性的元素。這些節點將與它們的子孫一起繪製到自己的層中,除非子孫由於上述一個(或多個)原因需要自己的層。

層確實能提高效能,但在記憶體管理方面成本很高,因此不應作為網頁效能最佳化策略的一部分過度使用。

合成

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

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

互動性

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

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

在我們的示例中,圖片可能載入得很快,但 another-script.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 秒,主執行緒在此期間完全被佔用,無法響應點選事件或螢幕點選。

另見