瀏覽器背后的運(yùn)行機(jī)制
從本章開始,我們的性能優(yōu)化探險(xiǎn)也正式進(jìn)入到了“深水區(qū)”——瀏覽器端的性能優(yōu)化。
平時(shí)我們幾乎每天都在和瀏覽器打交道,在一些兼容任務(wù)比較繁重的團(tuán)隊(duì)里,苦逼的前端攻城師們甚至為了兼容各個(gè)瀏覽器而不斷地去測(cè)試和調(diào)試,還要在腦子中記下各種遇到的 BUG 及解決方案。即便如此瀏覽器工作原理是怎樣的,我們好像并沒有去主動(dòng)地關(guān)注和了解下瀏覽器的工作原理。我想如果我們對(duì)此做一點(diǎn)了解,在項(xiàng)目過程中就可以有效地避免一些問題,并對(duì)頁(yè)面性能做出相應(yīng)的改進(jìn)。
“知己知彼,百戰(zhàn)不殆”,今天,我們就一起來揭開瀏覽器渲染過程的神秘面紗!
瀏覽器的“心”
瀏覽器的“心”,說的就是瀏覽器的內(nèi)核。在研究瀏覽器微觀的運(yùn)行機(jī)制之前,我們首先要對(duì)瀏覽器內(nèi)核有一個(gè)宏觀的把握。
開篇我提到許多工程師因?yàn)闃I(yè)務(wù)需要,免不了需要去處理不同瀏覽器下代碼渲染結(jié)果的差異性。這些差異性正是因?yàn)闉g覽器內(nèi)核的不同而導(dǎo)致的——瀏覽器內(nèi)核決定了瀏覽器解釋網(wǎng)頁(yè)語法的方式。
瀏覽器內(nèi)核可以分成兩部分:渲染引擎( 或者 )和 JS 引擎。早期渲染引擎和 JS 引擎并沒有十分明確的區(qū)分,但隨著 JS 引擎越來越獨(dú)立,內(nèi)核也成了渲染引擎的代稱(下文我們將沿用這種叫法)。渲染引擎又包括了 HTML 解釋器、CSS 解釋器、布局、網(wǎng)絡(luò)、存儲(chǔ)、圖形、音視頻、圖片解碼器等等零部件。
目前市面上常見的瀏覽器內(nèi)核可以分為這四種:(IE)、Gecko(火狐)、Blink(、Opera)、()。
這里面大家最耳熟能詳?shù)目赡芫褪? 內(nèi)核了。很多同學(xué)可能會(huì)聽說過 的內(nèi)核就是 ,殊不知 內(nèi)核早已迭代為了 Blink。但是換湯不換藥,Blink 其實(shí)也是基于 衍生而來的一個(gè)分支,因此, 內(nèi)核仍然是當(dāng)下瀏覽器世界真正的霸主。
下面我們就以 為例,對(duì)現(xiàn)代瀏覽器的渲染過程進(jìn)行一個(gè)深度的剖析。
開啟瀏覽器渲染“黑盒”
什么是渲染過程?簡(jiǎn)單來說,渲染引擎根據(jù) HTML 文件描述構(gòu)建相應(yīng)的數(shù)學(xué)模型,調(diào)用瀏覽器各個(gè)零部件,從而將網(wǎng)頁(yè)資源代碼轉(zhuǎn)換為圖像結(jié)果,這個(gè)過程就是渲染過程(如下圖)。
從這個(gè)流程來看,瀏覽器呈現(xiàn)網(wǎng)頁(yè)這個(gè)過程,宛如一個(gè)黑盒。在這個(gè)神秘的黑盒中,有許多功能模塊,內(nèi)核內(nèi)部的實(shí)現(xiàn)正是這些功能模塊相互配合協(xié)同工作進(jìn)行的。其中我們最需要關(guān)注的,就是HTML 解釋器、CSS 解釋器、圖層布局計(jì)算模塊、視圖繪制模塊與 引擎這幾大模塊:
瀏覽器渲染過程解析
有了對(duì)零部件的了解打底,我們就可以一起來走一遍瀏覽器的渲染流程了。在瀏覽器里,每一個(gè)頁(yè)面的首次渲染都經(jīng)歷了如下階段(圖中箭頭不代表串行,有一些操作是并行進(jìn)行的,下文會(huì)說明):
幾棵重要的“樹”
上面的內(nèi)容沒有理解透徹?別著急,我們一起來捋一捋這個(gè)過程中的重點(diǎn)——樹!
為了使渲染過程更明晰一些,我們需要給這些”樹“們一個(gè)特寫:
基于這些“樹”,我們?cè)偈崂硪环?/p>
渲染過程說白了,首先是基于 HTML 構(gòu)建一個(gè) DOM 樹,這棵 DOM 樹與 CSS 解釋器解析出的 CSSOM 相結(jié)合,就有了布局渲染樹。最后瀏覽器以布局渲染樹為藍(lán)本,去計(jì)算布局并繪制圖像,我們頁(yè)面的初次渲染就大功告成了。
之后每當(dāng)一個(gè)新元素加入到這個(gè) DOM 樹當(dāng)中,瀏覽器便會(huì)通過 CSS 引擎查遍 CSS 樣式表,找到符合該元素的樣式規(guī)則應(yīng)用到這個(gè)元素上,然后再重新去繪制它。
有心的同學(xué)可能已經(jīng)在思考了,查表是個(gè)花時(shí)間的活,我怎么讓瀏覽器的查詢工作又快又好地實(shí)現(xiàn)呢?OK,講了這么多原理,我們終于引出了我們的第一個(gè)可轉(zhuǎn)化為代碼的優(yōu)化點(diǎn)——CSS 樣式表規(guī)則的優(yōu)化!
不做無用功:基于渲染流程的 CSS 優(yōu)化建議
在給出 CSS 選擇器方面的優(yōu)化建議之前,先告訴大家一個(gè)小知識(shí):CSS 引擎查找樣式表,對(duì)每條規(guī)則都按從右到左的順序去匹配。 看如下規(guī)則:
css">#myList li {}
這樣的寫法其實(shí)很常見。大家平時(shí)習(xí)慣了從左到右閱讀的文字閱讀方式,會(huì)本能地以為瀏覽器也是從左到右匹配 CSS 選擇器的,因此會(huì)推測(cè)這個(gè)選擇器并不會(huì)費(fèi)多少力氣:# 是一個(gè) id 選擇器,它對(duì)應(yīng)的元素只有一個(gè),查找起來應(yīng)該很快。定位到了 元素,等于是縮小了范圍后再去查找它后代中的 li 元素,沒毛病。
事實(shí)上,CSS 選擇符是從右到左進(jìn)行匹配的。我們這個(gè)看似“沒毛病”的選擇器,實(shí)際開銷相當(dāng)高:瀏覽器必須遍歷頁(yè)面上每個(gè) li 元素,并且每次都要去確認(rèn)這個(gè) li 元素的父元素 id 是不是 ,你說坑不坑!
說到坑,不知道大家還記不記得這個(gè)經(jīng)典的通配符:
* {}
入門 CSS 的時(shí)候,不少同學(xué)拿通配符清除默認(rèn)樣式(我曾經(jīng)也是通配符用戶的一員)。但這個(gè)家伙很恐怖,它會(huì)匹配所有元素,所以瀏覽器必須去遍歷每一個(gè)元素!大家低頭看看自己頁(yè)面里的元素個(gè)數(shù),是不是心涼了——這得計(jì)算多少次呀!
這樣一看,一個(gè)小小的 CSS 選擇器,也有不少的門道!好的 CSS 選擇器書寫習(xí)慣,可以為我們帶來非??捎^的性能提升。根據(jù)上面的分析,我們至少可以總結(jié)出如下性能提升的方案:
false:
#myList li{}
true:
.myList_li {}
false:
.myList#title
true:
#title
搞定了 CSS 選擇器,萬里長(zhǎng)征才剛剛開始的第一步。但現(xiàn)在你已經(jīng)理解了瀏覽器的工作過程,接下來的征程對(duì)你來說并不再是什么難題~
告別阻塞:CSS 與 JS 的加載順序優(yōu)化
說完了過程,我們來說一說特性。
HTML、CSS 和 JS,都具有阻塞渲染的特性。
HTML 阻塞,天經(jīng)地義——沒有 HTML,何來 DOM?沒有 DOM,渲染和優(yōu)化,都是空談。
那么 CSS 和 JS 的阻塞又是怎么回事呢?
CSS 的阻塞
在剛剛的過程中,我們提到 DOM 和 CSSOM 合力才能構(gòu)建渲染樹。這一點(diǎn)會(huì)給性能造成嚴(yán)重影響:默認(rèn)情況下,CSS 是阻塞的資源。瀏覽器在構(gòu)建 CSSOM 的過程中,不會(huì)渲染任何已處理的內(nèi)容。即便 DOM 已經(jīng)解析完畢了,只要 CSSOM 不 OK,那么渲染這個(gè)事情就不 OK(這主要是為了避免沒有 CSS 的 HTML 頁(yè)面丑陋地“裸奔”在用戶眼前)。
我們知道,只有當(dāng)我們開始解析 HTML 后、解析到 link 標(biāo)簽或者 style 標(biāo)簽時(shí),CSS 才登場(chǎng),CSSOM 的構(gòu)建才開始。很多時(shí)候,DOM 不得不等待 CSSOM。因此我們可以這樣總結(jié):
CSS 是阻塞渲染的資源。需要將它盡早、盡快地下載到客戶端,以便縮短首次渲染的時(shí)間。
事實(shí)上,現(xiàn)在很多團(tuán)隊(duì)都已經(jīng)做到了盡早(將 CSS 放在 head 標(biāo)簽里)和盡快(啟用 CDN 實(shí)現(xiàn)靜態(tài)資源加載速度的優(yōu)化)。這個(gè)“把 CSS 往前放”的動(dòng)作,對(duì)很多同學(xué)來說已經(jīng)內(nèi)化為一種編碼習(xí)慣。那么現(xiàn)在我們還應(yīng)該知道,這個(gè)“習(xí)慣”不是空穴來風(fēng),它是由 CSS 的特性決定的。
JS 的阻塞
不知道大家注意到?jīng)]有,前面我們說過程的時(shí)候,花了很多筆墨去說 HTML、說 CSS。相比之下,JS 的出鏡率也太低了點(diǎn)。
這當(dāng)然不是因?yàn)?JS 不重要。而是因?yàn)椋谑状武秩具^程中,JS 并不是一個(gè)非登場(chǎng)不可的角色——沒有 JS,CSSOM 和 DOM 照樣可以組成渲染樹,頁(yè)面依然會(huì)呈現(xiàn)——即使它死氣沉沉、毫無交互。
JS 的作用在于修改,它幫助我們修改網(wǎng)頁(yè)的方方面面:內(nèi)容、樣式以及它如何響應(yīng)用戶交互。這“方方面面”的修改,本質(zhì)上都是對(duì) DOM 和 進(jìn)行修改。因此 JS 的執(zhí)行會(huì)阻止 CSSOM,在我們不作顯式聲明的情況下,它也會(huì)阻塞 DOM。
我們通過一個(gè)?來理解一下這個(gè)機(jī)制:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JS阻塞測(cè)試title>
<style>
#container {
background-color: yellow;
width: 100px;
height: 100px;
}
style>
<script>
// 嘗試獲取container元素
var container = document.getElementById("container")
console.log('container', container)

script>
head>
<body>
<div id="container">div>
<script>
// 嘗試獲取container元素
var container = document.getElementById("container")
console.log('container', container)
// 輸出container元素此刻的背景色
console.log('container bgColor', getComputedStyle(container).backgroundColor)
script>
<style>
#container {
background-color: blue;
}
style>
body>
html>
三個(gè) 的結(jié)果分別為:
注:本例僅使用了內(nèi)聯(lián) JS 做測(cè)試。感興趣的同學(xué)可以把這部分 JS 當(dāng)做外部文件引入看看效果——它們的表現(xiàn)一致。
第一次嘗試獲取 id 為 的 DOM 失敗,這說明 JS 執(zhí)行時(shí)阻塞了 DOM,后續(xù)的 DOM 無法構(gòu)建;第二次才成功,這說明腳本塊只能找到在它前面構(gòu)建好的元素。這兩者結(jié)合起來,“阻塞 DOM”得到了驗(yàn)證。再看第三個(gè) ,嘗試獲取 CSS 樣式,獲取到的是在 JS 代碼執(zhí)行前的背景色(),而非后續(xù)設(shè)定的新樣式(blue),說明 CSSOM 也被阻塞了。那么在阻塞的背后,到底發(fā)生了什么呢?
我們前面說過,JS 引擎是獨(dú)立于渲染引擎存在的。我們的 JS 代碼在文檔的何處插入,就在何處執(zhí)行。當(dāng) HTML 解析器遇到一個(gè) 標(biāo)簽時(shí),它會(huì)暫停渲染過程,將控制權(quán)交給 JS 引擎。JS 引擎對(duì)內(nèi)聯(lián)的 JS 代碼會(huì)直接執(zhí)行,對(duì)外部 JS 文件還要先獲取到腳本、再進(jìn)行執(zhí)行。等 JS 引擎運(yùn)行完畢,瀏覽器又會(huì)把控制權(quán)還給渲染引擎,繼續(xù) CSSOM 和 DOM 的構(gòu)建。 因此與其說是 JS 把 CSS 和 HTML 阻塞了瀏覽器工作原理是怎樣的,不如說是 JS 引擎搶走了渲染引擎的控制權(quán)。
現(xiàn)在理解了阻塞的表現(xiàn)與原理,我們開始思考一個(gè)問題。瀏覽器之所以讓 JS 阻塞其它的活動(dòng),是因?yàn)樗恢?JS 會(huì)做什么改變,擔(dān)心如果不阻止后續(xù)的操作,會(huì)造成混亂。但是我們是寫 JS 的人,我們知道 JS 會(huì)做什么改變。假如我們可以確認(rèn)一個(gè) JS 文件的執(zhí)行時(shí)機(jī)并不一定非要是此時(shí)此刻,我們就可以通過對(duì)它使用 defer 和 async 來避免不必要的阻塞,這里我們就引出了外部 JS 的三種加載方式。
JS的三種加載方式
<script src="index.js">script>
這種情況下 JS 會(huì)阻塞瀏覽器,瀏覽器必須等待 index.js 加載和執(zhí)行完畢才能去做其它事情。
<script async src="index.js">script>
async 模式下,JS 不會(huì)阻塞瀏覽器做任何其它的事情。它的加載是異步的,當(dāng)它加載結(jié)束,JS 腳本會(huì)立即執(zhí)行。
<script defer src="index.js">script>
defer 模式下,JS 的加載是異步的,執(zhí)行是被推遲的。等整個(gè)文檔解析完成、 事件即將被觸發(fā)時(shí),被標(biāo)記了 defer 的 JS 文件才會(huì)開始依次執(zhí)行。
從應(yīng)用的角度來說,一般當(dāng)我們的腳本與 DOM 元素和其它腳本之間的依賴關(guān)系不強(qiáng)時(shí),我們會(huì)選用 async;當(dāng)腳本依賴于 DOM 元素和其它腳本的執(zhí)行結(jié)果時(shí),我們會(huì)選用 defer。
通過審時(shí)度勢(shì)地向 標(biāo)簽添加 async/defer,我們就可以告訴瀏覽器在等待腳本可用期間不阻止其它的工作,這樣可以顯著提升性能。
小結(jié)
我們知道,當(dāng) JS 登場(chǎng)時(shí),往往意味著對(duì) DOM 的操作。DOM 操作所導(dǎo)致的性能開銷的“昂貴”,大家可能早就有所耳聞,雅虎軍規(guī)里很重要的一條就是“盡量減少 DOM 訪問”。
其它前端性能優(yōu)化: 前端技術(shù)架構(gòu)體系(沒有鏈接的后續(xù)跟進(jìn)): 其它相關(guān)
歡迎各位看官的批評(píng)和指正,共同學(xué)習(xí)和成長(zhǎng)
希望該文章對(duì)您有幫助,你的 支持和鼓勵(lì)會(huì)是我持續(xù)的動(dòng)力