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

前言

瀏覽器基礎(chǔ)是前端知識(shí)網(wǎng)中的一個(gè)小分支,也是前端開發(fā)人員必須掌握的基礎(chǔ)知識(shí)點(diǎn)。他貫穿著前端的整個(gè)網(wǎng)絡(luò)體系,項(xiàng)目優(yōu)化也是圍繞著瀏覽器進(jìn)行的。

開發(fā)人員在面試的時(shí)候或許會(huì)被問到:

從你在瀏覽器輸入一個(gè)網(wǎng)址到網(wǎng)頁內(nèi)容完全被展示的這段時(shí)間內(nèi),都發(fā)生了什么事情?

確實(shí)是個(gè)老生常談的問題,但問題的答案并不是唯一的,或許在三五年前,這個(gè)問題還會(huì)有一個(gè)「相對(duì)」標(biāo)準(zhǔn)的答案。

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

此答案精簡的概括了「后端為主的 MVC 模式」及早期 Web 應(yīng)用的瀏覽器響應(yīng)的全過程。前端技術(shù)發(fā)展到現(xiàn)在,「前后端分離」「中間件直出」和「MNV*模式」也已問世,再談及此問題,答案會(huì)有所不同。

就以「前后端分離」為例,在上方答案的第4步后,緊接著就不會(huì)直接進(jìn)入后端服務(wù)器了。而會(huì)被 HTTP 和反向代理服務(wù)器,如 Ngnix,攔截。

  • 前置步驟1、2、3、4
  • Ngnix 在監(jiān)聽到 HTTP(80端口)或 HTTPS(443端口)請(qǐng)求,根據(jù) URL 做服務(wù)分發(fā),分發(fā)(rewrite)到后端服務(wù)器或靜態(tài)資源服務(wù)器,首頁請(qǐng)求基本是分發(fā)到靜態(tài)服務(wù)器,返回一個(gè) HTML 文件
  • 步驟7、8、9、10
  • 執(zhí)行 JS 腳本,異步 ajax、 fetch 發(fā)起 POST、 GET 請(qǐng)求,重新進(jìn)入 Ngnix 分發(fā),此次分發(fā)到后端服務(wù)器,步驟5、6、7,然后返回一個(gè) xml 或 json 格式的信息,一般含有 code(返回碼)和 result(依賴信息)
  • js 回調(diào)根據(jù)返回碼執(zhí)行不同的邏輯,增刪改頁面元素,此時(shí)可能會(huì)發(fā)生重排或重繪。首頁加載結(jié)束。

從以上步驟可以發(fā)現(xiàn),瀏覽器可能會(huì)觸發(fā)兩次重繪,極易產(chǎn)生「白屏」或「頁面抖動(dòng)」現(xiàn)象,為了解決這個(gè)問題「中間件直出」的模式應(yīng)運(yùn)而生。另外為了擴(kuò)充大前端的陣營,吸納 IOS 和 Android,Google 設(shè)計(jì)了「MNV*模式」,典型代表就是 ReactNative,但此模式已經(jīng)脫離了瀏覽器的范疇,此處就不再做擴(kuò)展。

以上討論的渲染過程中使用到了較多的瀏覽器功能,如用戶地址欄輸入框、網(wǎng)絡(luò)請(qǐng)求、瀏覽器文檔解析、渲染引擎渲染網(wǎng)頁、 JavaScript 引擎執(zhí)行 js 腳本、客戶端存儲(chǔ)等。 接下來我們介紹下瀏覽器的基本結(jié)構(gòu)組成。

瀏覽器的結(jié)構(gòu)組成

瀏覽器一般由七個(gè)模塊組成,User Interface(用戶界面)、Browser engine(瀏覽器引擎)、Rendering engine(渲染引擎)、Networking(網(wǎng)絡(luò))、JavaScript Interpreter(js解釋器)、UI Backend(UI 后端)、Date Persistence(數(shù)據(jù)持久化存儲(chǔ)) 如下圖:

瀏覽器的結(jié)構(gòu)組成
  • 用戶界面 -包括地址欄、后退/前進(jìn)按鈕、書簽?zāi)夸浀?,也就是你所看到的除了頁面顯示窗口之外的其他部分
  • 瀏覽器引擎 -可以在用戶界面和渲染引擎之間傳送指令或在客戶端本地緩存中讀寫數(shù)據(jù)等,是瀏覽器中各個(gè)部分之間相互通信的核心
  • 渲染引擎 -解析DOM文檔和CSS規(guī)則并將內(nèi)容排版到瀏覽器中顯示有樣式的界面,也有人稱之為排版引擎,我們常說的瀏覽器內(nèi)核主要指的就是渲染引擎
  • 網(wǎng)絡(luò) -用來完成網(wǎng)絡(luò)調(diào)用或資源下載的模塊
  • UI 后端 -用來繪制基本的瀏覽器窗口內(nèi)控件,如輸入框、按鈕、單選按鈕等,根據(jù)瀏覽器不同繪制的視覺效果也不同,但功能都是一樣的。
  • JS解釋器 -用來解釋執(zhí)行JS腳本的模塊,如 V8 引擎、JavaScriptCore
  • 數(shù)據(jù)存儲(chǔ) -瀏覽器在硬盤中保存 cookie、localStorage等各種數(shù)據(jù),可通過瀏覽器引擎提供的API進(jìn)行調(diào)用

作為前端開發(fā)人員,我們需要重點(diǎn)理解渲染引擎的工作原理,靈活應(yīng)用數(shù)據(jù)存儲(chǔ)技術(shù),在實(shí)際項(xiàng)目開發(fā)中會(huì)經(jīng)常涉及到這兩個(gè)部分,尤其是在做項(xiàng)目性能優(yōu)化時(shí),理解瀏覽器渲染引擎的工作原理尤為重要。而其他部分則是由瀏覽器自行管理的,開發(fā)者能控制的地方較少。今天我們就圍繞這兩個(gè)重點(diǎn)其中的一個(gè)部分「瀏覽器渲染引擎」進(jìn)行展開

瀏覽器渲染引擎

瀏覽器渲染引擎是由各大瀏覽器廠商依照 W3C 標(biāo)準(zhǔn)自行研發(fā)的,也被稱之為「瀏覽器內(nèi)核」。

目前,市面上使用的主流瀏覽器內(nèi)核有5類:Trident、Gecko、Presto、Webkit、Blink。

Trident:俗稱 IE 內(nèi)核,也被叫做 MSHTML 引擎,目前在使用的瀏覽器有 IE11 -,以及各種國產(chǎn)多核瀏覽器中的IE兼容模塊。另外微軟的 Edge 瀏覽器不再使用 MSHTML 引擎,而是使用類全新的引擎 EdgeHTML。

Gecko:俗稱 Firefox 內(nèi)核,Netscape6 開始采用的內(nèi)核,后來的 Mozilla FireFox(火狐瀏覽器)也采用了該內(nèi)核,Gecko 的特點(diǎn)是代碼完全公開,因此,其可開發(fā)程度很高,全世界的程序員都可以為其編寫代碼,增加功能。因?yàn)檫@是個(gè)開源內(nèi)核,因此受到許多人的青睞,Gecko 內(nèi)核的瀏覽器也很多,這也是 Gecko 內(nèi)核雖然年輕但市場(chǎng)占有率能夠迅速提高的重要原因。

Presto:Opera 前內(nèi)核,為啥說是前內(nèi)核呢?因?yàn)?Opera12.17 以后便擁抱了 Google Chrome 的 Blink 內(nèi)核,此內(nèi)核就沒了寄托

Webkit:Safari 內(nèi)核,也是 Chrome 內(nèi)核原型,主要是 Safari 瀏覽器在使用的內(nèi)核,也是特性上表現(xiàn)較好的瀏覽器內(nèi)核。也被大量使用在移動(dòng)端瀏覽器上。

Blink: 由 Google 和 Opera Software 開發(fā),在Chrome(28及往后版本)、Opera(15及往后版本)和Yandex瀏覽器中使用。Blink 其實(shí)是 Webkit 的一個(gè)分支,添加了一些優(yōu)化的新特性,例如跨進(jìn)程的 iframe,將 DOM 移入 JavaScript 中來提高 JavaScript 對(duì) DOM 的訪問速度等,目前較多的移動(dòng)端應(yīng)用內(nèi)嵌的瀏覽器內(nèi)核也漸漸開始采用 Blink。

渲染引擎的工作流程

瀏覽器渲染引擎最重要的工作就是將 HTML 和 CSS 文檔解析組合最終渲染到瀏覽器窗口上。如下圖所示,渲染引擎在接受到 HTML 文件后主要進(jìn)行了以下操作:解析 HTML 構(gòu)建 DOM 樹 -> 構(gòu)建渲染樹 -> 渲染樹布局 -> 渲染樹繪制。

渲染引擎工作流程

解析 HTML 構(gòu)建 DOM 樹時(shí)渲染引擎會(huì)將 HTML 文件的便簽元素解析成多個(gè) DOM 元素對(duì)象節(jié)點(diǎn),并且將這些節(jié)點(diǎn)根據(jù)父子關(guān)系組成一個(gè)樹結(jié)構(gòu)。同時(shí) CSS 文件被解析成 CSS 規(guī)則表,然后將每條 CSS 規(guī)則按照「從右向左」的方式在 DOM 樹上進(jìn)行逆向匹配,生成一個(gè)具有樣式規(guī)則描述的 DOM 渲染樹。接下來就是將渲染樹進(jìn)行布局、繪制的過程。首先根據(jù) DOM 渲染樹上的樣式規(guī)則,對(duì) DOM 元素進(jìn)行大小和位置的定位,關(guān)鍵屬性如position;width;margin;padding;top;border;...,接下來再根據(jù)元素樣式規(guī)則中的color;background;shadow;...規(guī)則進(jìn)行繪制。

另外,這個(gè)過程是逐步完成的,為了更好的用戶體驗(yàn),渲染引擎將會(huì)盡可能早的將內(nèi)容呈現(xiàn)到屏幕上,并不會(huì)等到所有的 html 都解析完成之后再去構(gòu)建和布局 render 樹。它是解析完一部分內(nèi)容就顯示一部分內(nèi)容,同時(shí),可能還在通過網(wǎng)絡(luò)下載其余內(nèi)容。

再者,需要注意的是,在瀏覽器渲染完首屏頁面后,如果對(duì) DOM 進(jìn)行操作會(huì)引起瀏覽器引擎對(duì) DOM 渲染樹的重新布局和重新繪制,我們叫做「重排」和「重繪」,由于重排和重繪是前后依賴的關(guān)系,重繪發(fā)生時(shí)未必會(huì)觸發(fā)渲染引擎的重排,但是如果發(fā)生了重排就必然會(huì)觸發(fā)重繪操作,這樣帶來的性能損害就是巨大的。因此我們?cè)谧鲂阅軆?yōu)化的時(shí)候應(yīng)該遵循「避免重排;減少重繪」的原則。

不同瀏覽器內(nèi)核間的差異

在不同的瀏覽器內(nèi)核下, 瀏覽器頁面渲染的流程略有不同

webkit 內(nèi)核工作流程
Geoko 內(nèi)核工作流程

上面兩幅圖分別是 Webkit 和 Geoko 內(nèi)核渲染 DOM 的工作流程,對(duì)比可以看出,兩者的區(qū)別主要在于 CSS 樣式表的解析時(shí)機(jī),Webkit 內(nèi)核下,HTML 和 CSS 文件的解析是同步的,而 Geoko 內(nèi)核下,CSS 文件需要等到 HTML 文件解析成內(nèi)容 Sink 后才進(jìn)行解析。

另外描述術(shù)語也有不同,除此之外兩者的流程就基本相同了,其中最重要的三個(gè)部分就是 「HTML 的解析」「CSS 的解析」「渲染樹的生成」。這三個(gè)部分的原理比較深,會(huì)涉及到「詞法分析」「語法分析」「轉(zhuǎn)換」「解釋」等數(shù)據(jù)結(jié)構(gòu)的知識(shí),比較枯燥,一般我們了解到這里就夠了,想深入了解的同學(xué)可以閱讀此篇譯文,瀏覽器的工作原理,里面詳細(xì)的解釋了以上三個(gè)部分的流程和原理。此處就不再多做贅述了。

關(guān)于 CSS 規(guī)則的匹配

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

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

我們重新回看【webkit 內(nèi)核工作流程圖】

webkit 內(nèi)核工作流程

CSS 規(guī)則匹配是發(fā)生在webkit引擎的「Attachment」過程中,瀏覽器要為每個(gè) DOM Tree 中的元素?cái)U(kuò)充 CSS 樣式規(guī)則(匹配 Style Rules)。對(duì)于每個(gè) DOM 元素,必須在所有 Style Rules 中找到符合的 selector 并將對(duì)應(yīng)的規(guī)則進(jìn)行合并。選擇器的「解析」實(shí)際是在這里執(zhí)行的,在遍歷 DOM Tree 時(shí),從 Style Rules 中去尋找對(duì)應(yīng)的 selector。

我們來舉一個(gè)最簡單的栗子:

<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>

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

此處會(huì)有 4*3 個(gè)匹配項(xiàng),如果做正向匹配,在遇到 <span> 標(biāo)簽匹配 div .t p{ color: red; } 到匹配項(xiàng)時(shí),計(jì)算機(jī)首先要找到<span> 標(biāo)簽的父標(biāo)簽和祖父標(biāo)簽,判斷他們是否滿足div .t的規(guī)則,然后再匹配<span>是否為p標(biāo)簽,此處匹配不成功,產(chǎn)生了三次浪費(fèi)。

如果時(shí)逆向匹配,那么第一次對(duì)比<span>是否為p標(biāo)簽便可排除此規(guī)則,效率更高。

如果將 HTML 結(jié)構(gòu)變復(fù)雜,CSS 規(guī)則表變龐大,那么,「逆向匹配」的優(yōu)勢(shì)就遠(yuǎn)大于「正向匹配」了,因?yàn)槠ヅ涞那闆r遠(yuǎn)遠(yuǎn)低于不匹配的情況。另外,如果在選擇器結(jié)尾加上通配符「*」,那么「逆向匹配」的優(yōu)勢(shì)就大打折扣了,這也就是很多優(yōu)化原則提到的「盡量避免在選擇器末尾添加通配符」的原因。

極限了想,如果我們的樣式表不存在嵌套關(guān)系,如下:

<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」過程將得到極大的精簡,效率也是可想而知的,這就是為什么「微信小程序」樣式表不建議使用關(guān)系行寫法的原因。

相關(guān)的性能優(yōu)化

我們大致可以在以上案例中看到同瀏覽器渲染引擎相關(guān)的可行優(yōu)化點(diǎn)。

大致為以下幾種

減少 JS 加載對(duì) Dom 渲染的影響

將 JS 文件放在 HTML 文檔后加載,或者使用異步的方式加載 JS 代碼

避免重排,減少重繪

在做 css 動(dòng)畫的時(shí)候減少使用 width、 margin、 padding 等影響 CSS 布局對(duì)規(guī)則,可以使用 CSS3 的 transform 代替。另外值得注意的是,在加載大量的圖片元素時(shí),盡量預(yù)先限定圖片的尺寸大小,否則在圖片加載過程中會(huì)更新圖片的排版信息,產(chǎn)生大量的重排。

減少使用關(guān)系型樣式表的寫法

直接使用唯一的類名即可最大限度的提升渲染效率,另外盡量避免在選擇器末尾添加通配符

減少 DOM 的層級(jí)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容