「前端那些事兒」① 瀏覽器渲染引擎

木_羽_發表於2017-10-31

前言

瀏覽器基礎是前端知識網中的一個小分支,也是前端開發人員必須掌握的基礎知識點。他貫穿著前端的整個網路體系,專案優化也是圍繞著瀏覽器進行的。

開發人員在面試的時候或許會被問到:

從你在瀏覽器輸入一個網址到網頁內容完全被展示的這段時間內,都發生了什麼事情?

確實是個老生常談的問題,但問題的答案並不是唯一的,或許在三五年前,這個問題還會有一個「相對」標準的答案。

  1. 瀏覽器在接收到這個指令時,會開啟一個單獨的執行緒來處理這個指令,首先要判斷使用者輸入的是否為合法或合理的 URL 地址,是否為 HTTP 協議請求,如果是那就進入下一步
  2. 瀏覽器的瀏覽器引擎將對此 URL 進行分析,如果存在快取「cache-control」且未過期,則會從本地快取提取檔案(From Memory Cache,200返回碼),如果快取「cache-control」不存在或過期,瀏覽器將發起遠端請求
  3. 通過 DNS 解析域名獲取該網站地址對應的 IP 地址,連同瀏覽器的 Cookie、 userAgent 等資訊向此 IP 發出 GET 請求。
  4. 接下來就是經典的「三次握手」,HTTP 協議會話,瀏覽器客戶端向 Web 伺服器傳送報文,進行通訊和資料傳輸。
  5. 進入網站的後端服務,如 Tomcat、Apache 等,還有近幾年流行的 Node.js 伺服器,這些伺服器上部署著應用程式碼,語言有很多,如 Java、 PHP、 C++、 C# 和 Javascript 等。
  6. 伺服器根據 URL 執行相應的後端應用邏輯,期間會使用到「伺服器快取」或「資料庫」。
  7. 伺服器處理請求並返回響應報文,如果瀏覽器訪問過該頁面,快取上有對應資源,與伺服器最後修改記錄對比,一致則返回 304,否則返回 200 和對應的內容。
  8. 瀏覽器接收到返回資訊並開始下載該 HTML檔案(無快取、200返回碼)或從本地快取提取檔案(有快取、304返回碼)
  9. 瀏覽器的渲染引擎在拿到 HTML 檔案後,便開始解析構建 DOM 樹,並根據 HTML 中的標記請求下載指定的 MIME 型別檔案(如 CSS、 JavaScript 指令碼等),同時使用&設定快取等內容。
  10. 渲染引擎根據 CSS 樣式規則將 DOM 樹擴充為渲染樹,然後進行重排、重繪。
  11. 如果含有 JS 檔案將會執行,進行 Dom 操作、快取讀存、事件繫結等操作。最終頁面將被展示在瀏覽器上。

此答案精簡的概括了「後端為主的 MVC 模式」及早期 Web 應用的瀏覽器響應的全過程。前端技術發展到現在,「前後端分離」「中介軟體直出」和「MNV*模式」也已問世,再談及此問題,答案會有所不同。

就以「前後端分離」為例,在上方答案的第4步後,緊接著就不會直接進入後端伺服器了。而會被 HTTP 和反向代理伺服器,如 Ngnix,攔截。

  • 前置步驟1、2、3、4
  • Ngnix 在監聽到 HTTP(80埠)或 HTTPS(443埠)請求,根據 URL 做服務分發,分發(rewrite)到後端伺服器或靜態資源伺服器,首頁請求基本是分發到靜態伺服器,返回一個 HTML 檔案
  • 步驟7、8、9、10
  • 執行 JS 指令碼,非同步 ajax、 fetch 發起 POST、 GET 請求,重新進入 Ngnix 分發,此次分發到後端伺服器,步驟5、6、7,然後返回一個 xml 或 json 格式的資訊,一般含有 code(返回碼)和 result(依賴資訊)
  • js 回撥根據返回碼執行不同的邏輯,增刪改頁面元素,此時可能會發生重排或重繪。首頁載入結束。

從以上步驟可以發現,瀏覽器可能會觸發兩次重繪,極易產生「白屏」或「頁面抖動」現象,為了解決這個問題「中介軟體直出」的模式應運而生。另外為了擴充大前端的陣營,吸納 IOS 和 Android,Google 設計了「MNV*模式」,典型代表就是 ReactNative,但此模式已經脫離了瀏覽器的範疇,此處就不再做擴充套件。

以上討論的渲染過程中使用到了較多的瀏覽器功能,如使用者位址列輸入框、網路請求、瀏覽器文件解析、渲染引擎渲染網頁、 JavaScript 引擎執行 js 指令碼、客戶端儲存等。 接下來我們介紹下瀏覽器的基本結構組成。

瀏覽器的結構組成

瀏覽器一般由七個模組組成,User Interface(使用者介面)、Browser engine(瀏覽器引擎)、Rendering engine(渲染引擎)、Networking(網路)、JavaScript Interpreter(js直譯器)、UI Backend(UI 後端)、Date Persistence(資料持久化儲存) 如下圖:

瀏覽器的結構組成
瀏覽器的結構組成

  • 使用者介面 -包括位址列、後退/前進按鈕、書籤目錄等,也就是你所看到的除了頁面顯示視窗之外的其他部分
  • 瀏覽器引擎 -可以在使用者介面和渲染引擎之間傳送指令或在客戶端本地快取中讀寫資料等,是瀏覽器中各個部分之間相互通訊的核心
  • 渲染引擎 -解析DOM文件和CSS規則並將內容排版到瀏覽器中顯示有樣式的介面,也有人稱之為排版引擎,我們常說的瀏覽器核心主要指的就是渲染引擎
  • 網路 -用來完成網路呼叫或資源下載的模組
  • UI 後端 -用來繪製基本的瀏覽器視窗內控制元件,如輸入框、按鈕、單選按鈕等,根據瀏覽器不同繪製的視覺效果也不同,但功能都是一樣的。
  • JS直譯器 -用來解釋執行JS指令碼的模組,如 V8 引擎、JavaScriptCore
  • 資料儲存 -瀏覽器在硬碟中儲存 cookie、localStorage等各種資料,可通過瀏覽器引擎提供的API進行呼叫

作為前端開發人員,我們需要重點理解渲染引擎的工作原理,靈活應用資料儲存技術,在實際專案開發中會經常涉及到這兩個部分,尤其是在做專案效能優化時,理解瀏覽器渲染引擎的工作原理尤為重要。而其他部分則是由瀏覽器自行管理的,開發者能控制的地方較少。今天我們就圍繞這兩個重點其中的一個部分「瀏覽器渲染引擎」進行展開

瀏覽器渲染引擎

瀏覽器渲染引擎是由各大瀏覽器廠商依照 W3C 標準自行研發的,也被稱之為「瀏覽器核心」。

目前,市面上使用的主流瀏覽器核心有5類:Trident、Gecko、Presto、Webkit、Blink。

Trident:俗稱 IE 核心,也被叫做 MSHTML 引擎,目前在使用的瀏覽器有 IE11 -,以及各種國產多核瀏覽器中的IE相容模組。另外微軟的 Edge 瀏覽器不再使用 MSHTML 引擎,而是使用類全新的引擎 EdgeHTML。

Gecko:俗稱 Firefox 核心,Netscape6 開始採用的核心,後來的 Mozilla FireFox(火狐瀏覽器)也採用了該核心,Gecko 的特點是程式碼完全公開,因此,其可開發程度很高,全世界的程式設計師都可以為其編寫程式碼,增加功能。因為這是個開源核心,因此受到許多人的青睞,Gecko 核心的瀏覽器也很多,這也是 Gecko 核心雖然年輕但市場佔有率能夠迅速提高的重要原因。

Presto:Opera 前核心,為啥說是前核心呢?因為 Opera12.17 以後便擁抱了 Google Chrome 的 Blink 核心,此核心就沒了寄託

Webkit:Safari 核心,也是 Chrome 核心原型,主要是 Safari 瀏覽器在使用的核心,也是特性上表現較好的瀏覽器核心。也被大量使用在移動端瀏覽器上。

Blink: 由 Google 和 Opera Software 開發,在Chrome(28及往後版本)、Opera(15及往後版本)和Yandex瀏覽器中使用。Blink 其實是 Webkit 的一個分支,新增了一些優化的新特性,例如跨程式的 iframe,將 DOM 移入 JavaScript 中來提高 JavaScript 對 DOM 的訪問速度等,目前較多的移動端應用內嵌的瀏覽器核心也漸漸開始採用 Blink。

渲染引擎的工作流程

瀏覽器渲染引擎最重要的工作就是將 HTML 和 CSS 文件解析組合最終渲染到瀏覽器視窗上。如下圖所示,渲染引擎在接受到 HTML 檔案後主要進行了以下操作:解析 HTML 構建 DOM 樹 -> 構建渲染樹 -> 渲染樹佈局 -> 渲染樹繪製。

渲染引擎工作流程
渲染引擎工作流程

解析 HTML 構建 DOM 樹時渲染引擎會將 HTML 檔案的便籤元素解析成多個 DOM 元素物件節點,並且將這些節點根據父子關係組成一個樹結構。同時 CSS 檔案被解析成 CSS 規則表,然後將每條 CSS 規則按照「從右向左」的方式在 DOM 樹上進行逆向匹配,生成一個具有樣式規則描述的 DOM 渲染樹。接下來就是將渲染樹進行佈局、繪製的過程。首先根據 DOM 渲染樹上的樣式規則,對 DOM 元素進行大小和位置的定位,關鍵屬性如position;width;margin;padding;top;border;...,接下來再根據元素樣式規則中的color;background;shadow;...規則進行繪製。

另外,這個過程是逐步完成的,為了更好的使用者體驗,渲染引擎將會盡可能早的將內容呈現到螢幕上,並不會等到所有的 html 都解析完成之後再去構建和佈局 render 樹。它是解析完一部分內容就顯示一部分內容,同時,可能還在通過網路下載其餘內容。

再者,需要注意的是,在瀏覽器渲染完首屏頁面後,如果對 DOM 進行操作會引起瀏覽器引擎對 DOM 渲染樹的重新佈局和重新繪製,我們叫做「重排」和「重繪」,由於重排和重繪是前後依賴的關係,重繪發生時未必會觸發渲染引擎的重排,但是如果發生了重排就必然會觸發重繪操作,這樣帶來的效能損害就是巨大的。因此我們在做效能優化的時候應該遵循「避免重排;減少重繪」的原則。

不同瀏覽器核心間的差異

在不同的瀏覽器核心下, 瀏覽器頁面渲染的流程略有不同

webkit 核心工作流程
webkit 核心工作流程

Geoko 核心工作流程
Geoko 核心工作流程

上面兩幅圖分別是 Webkit 和 Geoko 核心渲染 DOM 的工作流程,對比可以看出,兩者的區別主要在於 CSS 樣式表的解析時機,Webkit 核心下,HTML 和 CSS 檔案的解析是同步的,而 Geoko 核心下,CSS 檔案需要等到 HTML 檔案解析成內容 Sink 後才進行解析。

另外描述術語也有不同,除此之外兩者的流程就基本相同了,其中最重要的三個部分就是 「HTML 的解析」「CSS 的解析」「渲染樹的生成」。這三個部分的原理比較深,會涉及到「詞法分析」「語法分析」「轉換」「解釋」等資料結構的知識,比較枯燥,一般我們瞭解到這裡就夠了,想深入瞭解的同學可以閱讀此篇譯文,瀏覽器的工作原理,裡面詳細的解釋了以上三個部分的流程和原理。此處就不再多做贅述了。

關於 CSS 規則的匹配

上面我們提到過, CSS 規則是按照「從右向左」的方式在 DOM 樹上進行逆向匹配的,最終生成一個具有樣式規則描述的 DOM 渲染樹。

但是你知道為什麼要「從右向左」做逆向匹配嗎?

我們重新回看【webkit 核心工作流程圖】

webkit 核心工作流程
webkit 核心工作流程

CSS 規則匹配是發生在webkit引擎的「Attachment」過程中,瀏覽器要為每個 DOM Tree 中的元素擴充 CSS 樣式規則(匹配 Style Rules)。對於每個 DOM 元素,必須在所有 Style Rules 中找到符合的 selector 並將對應的規則進行合併。選擇器的「解析」實際是在這裡執行的,在遍歷 DOM Tree 時,從 Style Rules 中去尋找對應的 selector。

我們來舉一個最簡單的栗子:

<template>
<div>
  <div class="t">
    <span>test</span>
    <p>test</p>
  <div>
</div>
</template>

<style>
div{ color: #000; }
div .t span{ color: red; }
div .t p{color: blue; }
</style>複製程式碼

此處我們有一個 html 元素 和一個 style 元素,兩者需要做遍歷匹配

此處會有 4*3 個匹配項,如果做正向匹配,在遇到 <span> 標籤匹配 div .t p{ color: red; } 到匹配項時,計算機首先要找到<span> 標籤的父標籤和祖父標籤,判斷他們是否滿足div .t的規則,然後再匹配<span>是否為p標籤,此處匹配不成功,產生了三次浪費。

如果時逆向匹配,那麼第一次對比<span>是否為p標籤便可排除此規則,效率更高。

如果將 HTML 結構變複雜,CSS 規則表變龐大,那麼,「逆向匹配」的優勢就遠大於「正向匹配」了,因為匹配的情況遠遠低於不匹配的情況。另外,如果在選擇器結尾加上萬用字元「*」,那麼「逆向匹配」的優勢就大打折扣了,這也就是很多優化原則提到的「儘量避免在選擇器末尾新增萬用字元」的原因。

極限了想,如果我們的樣式表不存在巢狀關係,如下:

<template>
  <div class="t">
    <span class="div_t_span">test</span>
    <p class="div_t_p">test</p>
  <div>
</template>

<style>
div{ color: #000; }
.div_t_span{ color: red; }
.div_t_p{color: blue; }
</style複製程式碼

那麼引擎的「Attachment」過程將得到極大的精簡,效率也是可想而知的,這就是為什麼「微信小程式」樣式表不建議使用關係行寫法的原因。

相關的效能優化

我們大致可以在以上案例中看到同瀏覽器渲染引擎相關的可行優化點。

大致為以下幾種

減少 JS 載入對 Dom 渲染的影響

將 JS 檔案放在 HTML 文件後載入,或者使用非同步的方式載入 JS 程式碼

避免重排,減少重繪

在做 css 動畫的時候減少使用 width、 margin、 padding 等影響 CSS 佈局對規則,可以使用 CSS3 的 transform 代替。另外值得注意的是,在載入大量的圖片元素時,儘量預先限定圖片的尺寸大小,否則在圖片載入過程中會更新圖片的排版資訊,產生大量的重排。

減少使用關係型樣式表的寫法

直接使用唯一的類名即可最大限度的提升渲染效率,另外儘量避免在選擇器末尾新增萬用字元

減少 DOM 的層級

減少無意義的 dom 層級可以減少 渲染引擎 Attachment 過程中的匹配計算量

預告

「前端那些事兒系列」二 關於前端優化策略

相關文章