平時咱們幾乎天天都在和瀏覽器打交道,在一些兼容任務比較繁重的團隊裏,苦逼的前端攻城師們甚至爲了兼容各個瀏覽器而不斷地去測試和調試,還要在腦子中記下各類遇到的 BUG 及解決方案。即使如此,咱們好像並無去主動地關注和了解下瀏覽器的工做原理。我想若是咱們對此作一點了解,在項目過程當中就能夠有效地避免一些問題,並對頁面性能作出相應的改進。html
「知己知彼,百戰不殆」,如今咱們就一塊兒來揭開瀏覽器渲染過程的神祕面紗!前端
瀏覽器的「心」,說的就是瀏覽器的內核。在研究瀏覽器微觀的運行機制以前,咱們首先要對瀏覽器內核有一個宏觀的把握。後端
開篇我提到許多工程師由於業務須要,免不了須要去處理不一樣瀏覽器下代碼渲染結果的差別性。這些差別性正是由於瀏覽器內核的不一樣而致使的——瀏覽器內核決定了瀏覽器解釋網頁語法的方式。
瀏覽器內核能夠分紅兩部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎並無十分明確的區分,但隨着 JS 引擎愈來愈獨立,內核也成了渲染引擎的代稱(下文咱們將沿用這種叫法)。渲染引擎又包括了 HTML 解釋器、CSS 解釋器、佈局、網絡、存儲、圖形、音視頻、圖片解碼器等等零部件。瀏覽器
目前市面上常見的瀏覽器內核能夠分爲這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。bash
這裏面你們最耳熟能詳的可能就是 Webkit 內核了。不少同窗可能會據說過 Chrome 的內核就是 Webkit,卻不知 Chrome 內核早已迭代爲了 Blink。可是換湯不換藥,Blink 其實也是基於 Webkit 衍生而來的一個分支,所以,Webkit 內核仍然是當下瀏覽器世界真正的霸主。網絡
下面咱們就以 Webkit 爲例,對現代瀏覽器的渲染過程進行一個深度的剖析。異步
什麼是渲染過程?簡單來講,渲染引擎根據 HTML 文件描述構建相應的數學模型,調用瀏覽器各個零部件,從而將網頁資源代碼轉換爲圖像結果,這個過程就是渲染過程(以下圖)。async
從這個流程來看,瀏覽器呈現網頁這個過程,宛如一個黑盒。在這個神祕的黑盒中,有許多功能模塊,內核內部的實現正是這些功能模塊相互配合協同工做進行的。其中咱們最須要關注的,就是HTML 解釋器、CSS 解釋器、圖層佈局計算模塊、視圖繪製模塊與JavaScript 引擎這幾大模塊:ide
HTML 解釋器:將 HTML 文檔通過詞法分析輸出 DOM 樹。佈局
CSS 解釋器:解析 CSS 文檔, 生成樣式規則。
圖層佈局計算模塊:佈局計算每一個對象的精確位置和大小。
視圖繪製模塊:進行具體節點的圖像繪製,將像素渲染到屏幕上。
JavaScript 引擎:編譯執行 Javascript 代碼。
有了對零部件的瞭解打底,咱們就能夠一塊兒來走一遍瀏覽器的渲染流程了。在瀏覽器裏,每個頁面的首次渲染都經歷了以下階段(圖中箭頭不表明串行,有一些操做是並行進行的,下文會說明):
在這一步瀏覽器執行了全部的加載解析邏輯,在解析 HTML 的過程當中發出了頁面渲染所需的各類外部資源請求。
瀏覽器將識別並加載全部的 CSS 樣式信息與 DOM 樹合併,最終生成頁面 render 樹(:after :before 這樣的僞元素會在這個環節被構建到 DOM 樹中)。
頁面中全部元素的相對位置信息,大小等信息均在這一步獲得計算。
在這一步中瀏覽器會根據咱們的 DOM 代碼結果,把每個頁面圖層轉換爲像素,並對全部的媒體文件進行解碼。
最後一步瀏覽器會合併合各個圖層,將數據由 CPU 輸出給 GPU 最終繪製在屏幕上。(複雜的視圖層會給這個階段的 GPU 計算帶來一些壓力,在實際應用中爲了優化動畫性能,咱們有時會手動區分不一樣的圖層)。
上面的內容沒有理解透徹?彆着急,咱們一塊兒來捋一捋這個過程當中的重點——樹!
爲了使渲染過程更明晰一些,咱們須要給這些」樹「們一個特寫:
DOM 樹:解析 HTML 以建立的是 DOM 樹(DOM tree ):渲染引擎開始解析 HTML 文檔,轉換樹中的標籤到 DOM 節點,它被稱爲「內容樹」。
CSSOM 樹:解析 CSS(包括外部 CSS 文件和樣式元素)建立的是 CSSOM 樹。CSSOM 的解析過程與 DOM 的解析過程是並行的。
渲染樹:CSSOM 與 DOM 結合,以後咱們獲得的就是渲染樹(Render tree )。
佈局渲染樹:從根節點遞歸調用,計算每個元素的大小、位置等,給每一個節點所應該出如今屏幕上的精確座標,咱們便獲得了基於渲染樹的佈局渲染樹(Layout of the render tree)。
繪製渲染樹: 遍歷渲染樹,每一個節點將使用 UI 後端層來繪製。整個過程叫作繪製渲染樹(Painting the render tree)。
基於這些「樹」,咱們再梳理一番:
渲染過程說白了,首先是基於 HTML 構建一個 DOM 樹,這棵 DOM 樹與 CSS 解釋器解析出的 CSSOM 相結合,就有了佈局渲染樹。最後瀏覽器以佈局渲染樹爲藍本,去計算佈局並繪製圖像,咱們頁面的初次渲染就大功告成了。
以後每當一個新元素加入到這個 DOM 樹當中,瀏覽器便會經過 CSS 引擎查遍 CSS 樣式表,找到符合該元素的樣式規則應用到這個元素上,而後再從新去繪製它。
有心的同窗可能已經在思考了,查表是個花時間的活,我怎麼讓瀏覽器的查詢工做又快又好地實現呢?OK,講了這麼多原理,咱們終於引出了咱們的第一個可轉化爲代碼的優化點——CSS 樣式表規則的優化!
在給出 CSS 選擇器方面的優化建議以前,先告訴你們一個小知識:CSS 引擎查找樣式表,對每條規則都按從右到左的順序去匹配。 看以下規則:
#myList li {}
複製代碼
這樣的寫法其實很常見。你們平時習慣了從左到右閱讀的文字閱讀方式,會本能地覺得瀏覽器也是從左到右匹配 CSS 選擇器的,所以會推測這個選擇器並不會費多少力氣:#myList 是一個 id 選擇器,它對應的元素只有一個,查找起來應該很快。定位到了 myList 元素,等因而縮小了範圍後再去查找它後代中的 li 元素,沒毛病。
事實上,CSS 選擇符是從右到左進行匹配的。咱們這個看似「沒毛病」的選擇器,實際開銷至關高:瀏覽器必須遍歷頁面上每一個 li 元素,而且每次都要去確認這個 li 元素的父元素 id 是否是 myList,你說坑不坑!
說到坑,不知道你們還記不記得這個經典的通配符:
* {}
複製代碼
入門 CSS 的時候,很多同窗拿通配符清除默認樣式(我曾經也是通配符用戶的一員)。但這個傢伙很恐怖,它會匹配全部元素,因此瀏覽器必須去遍歷每個元素!你們低頭看看本身頁面裏的元素個數,是否是心涼了——這得計算多少次呀!
這樣一看,一個小小的 CSS 選擇器,也有很多的門道!好的 CSS 選擇器書寫習慣,能夠爲咱們帶來很是可觀的性能提高。根據上面的分析,咱們至少能夠總結出以下性能提高的方案:
避免使用通配符,只對須要用到的元素進行選擇。
關注能夠經過繼承實現的屬性,避免重複匹配重複定義。
少用標籤選擇器。若是能夠,用類選擇器替代,舉個🌰:
錯誤示範:
#myList li{}
複製代碼
課表明:
.myList_li {}
複製代碼
不要多此一舉,id 和 class 選擇器不該該被多餘的標籤選擇器拖後腿。舉個🌰:
錯誤示範
.myList#title
複製代碼
課表明
#title
複製代碼
減小嵌套。後代選擇器的開銷是最高的,所以咱們應該儘可能將選擇器的深度降到最低(最高不要超過三層),儘量使用類來關聯每個標籤元素。
搞定了 CSS 選擇器,萬里長征纔剛剛開始的第一步。但如今你已經理解了瀏覽器的工做過程,接下來的征程對你來講並再也不是什麼難題~
說完了過程,咱們來講一說特性。
HTML、CSS 和 JS,都具備阻塞渲染的特性。
HTML 阻塞,天經地義——沒有 HTML,何來 DOM?沒有 DOM,渲染和優化,都是空談。
那麼 CSS 和 JS 的阻塞又是怎麼回事呢?
在剛剛的過程當中,咱們提到 DOM 和 CSSOM 協力才能構建渲染樹。這一點會給性能形成嚴重影響:默認狀況下,CSS 是阻塞的資源。瀏覽器在構建 CSSOM 的過程當中,不會渲染任何已處理的內容。即使 DOM 已經解析完畢了,只要 CSSOM 不 OK,那麼渲染這個事情就不 OK(這主要是爲了不沒有 CSS 的 HTML 頁面醜陋地「裸奔」在用戶眼前)。
咱們知道,只有當咱們開始解析 HTML 後、解析到 link 標籤或者 style 標籤時,CSS 才登場,CSSOM 的構建纔開始。不少時候,DOM 不得不等待 CSSOM。所以咱們能夠這樣總結:
CSS 是阻塞渲染的資源。須要將它儘早、儘快地下載到客戶端,以便縮短首次渲染的時間。
事實上,如今不少團隊都已經作到了儘早(將 CSS 放在 head 標籤裏)和儘快(啓用 CDN 實現靜態資源加載速度的優化)。這個「把 CSS 往前放」的動做,對不少同窗來講已經內化爲一種編碼習慣。那麼如今咱們還應該知道,這個「習慣」不是空穴來風,它是由 CSS 的特性決定的。
不知道你們注意到沒有,前面咱們說過程的時候,花了不少筆墨去說 HTML、說 CSS。相比之下,JS 的出鏡率也過低了點。
這固然不是由於 JS 不重要。而是由於,在首次渲染過程當中,JS 並非一個非登場不可的角色——沒有 JS,CSSOM 和 DOM 照樣能夠組成渲染樹,頁面依然會呈現——即便它死氣沉沉、毫無交互。
JS 的做用在於修改,它幫助咱們修改網頁的方方面面:內容、樣式以及它如何響應用戶交互。這「方方面面」的修改,本質上都是對 DOM 和 CSSDOM 進行修改。所以 JS 的執行會阻止 CSSOM,在咱們不做顯式聲明的狀況下,它也會阻塞 DOM。
咱們經過一個🌰來理解一下這個機制:
<!DOCTYPE html>
<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阻塞測試</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>
複製代碼
三個 console 的結果分別爲:
注:本例僅使用了內聯 JS 作測試。感興趣的同窗能夠把這部分 JS 當作外部文件引入看看效果——它們的表現一致。
第一次嘗試獲取 id 爲 container 的 DOM 失敗,這說明 JS 執行時阻塞了 DOM,後續的 DOM 沒法構建;第二次才成功,這說明腳本塊只能找到在它前面構建好的元素。這二者結合起來,「阻塞 DOM」獲得了驗證。再看第三個 console,嘗試獲取 CSS 樣式,獲取到的是在 JS 代碼執行前的背景色(yellow),而非後續設定的新樣式(blue),說明 CSSOM 也被阻塞了。那麼在阻塞的背後,到底發生了什麼呢?
咱們前面說過,JS 引擎是獨立於渲染引擎存在的。咱們的 JS 代碼在文檔的何處插入,就在何處執行。當 HTML 解析器遇到一個 script 標籤時,它會暫停渲染過程,將控制權交給 JS 引擎。JS 引擎對內聯的 JS 代碼會直接執行,對外部 JS 文件還要先獲取到腳本、再進行執行。等 JS 引擎運行完畢,瀏覽器又會把控制權還給渲染引擎,繼續 CSSOM 和 DOM 的構建。 所以與其說是 JS 把 CSS 和 HTML 阻塞了,不如說是 JS 引擎搶走了渲染引擎的控制權。
如今理解了阻塞的表現與原理,咱們開始思考一個問題。瀏覽器之因此讓 JS 阻塞其它的活動,是由於它不知道 JS 會作什麼改變,擔憂若是不阻止後續的操做,會形成混亂。可是咱們是寫 JS 的人,咱們知道 JS 會作什麼改變。假如咱們能夠確認一個 JS 文件的執行時機並不必定非要是此時此刻,咱們就能夠經過對它使用 defer 和 async 來避免沒必要要的阻塞,這裏咱們就引出了外部 JS 的三種加載方式。
正常模式:
<script src="index.js"></script>
複製代碼
這種狀況下 JS 會阻塞瀏覽器,瀏覽器必須等待 index.js 加載和執行完畢才能去作其它事情。
async 模式:
<script async src="index.js"></script>
複製代碼
async 模式下,JS 不會阻塞瀏覽器作任何其它的事情。它的加載是異步的,當它加載結束,JS 腳本會當即執行。
defer 模式:
<script defer src="index.js"></script>
複製代碼
defer 模式下,JS 的加載是異步的,執行是被推遲的。等整個文檔解析完成、DOMContentLoaded 事件即將被觸發時,被標記了 defer 的 JS 文件纔會開始依次執行。
從應用的角度來講,通常當咱們的腳本與 DOM 元素和其它腳本之間的依賴關係不強時,咱們會選用 async;當腳本依賴於 DOM 元素和其它腳本的執行結果時,咱們會選用 defer。
經過審時度勢地向 script 標籤添加 async/defer,咱們就能夠告訴瀏覽器在等待腳本可用期間不阻止其它的工做,這樣能夠顯著提高性能。
咱們知道,當 JS 登場時,每每意味着對 DOM 的操做。DOM 操做所致使的性能開銷的「昂貴」,你們可能早就有所耳聞,雅虎軍規裏很重要的一條就是「儘可能減小 DOM 訪問」。
那麼 DOM 到底爲何慢,咱們如何去規避這種慢呢?這裏咱們就引出了須要重點解釋的兩個概念:CSS 中的迴流(Reflow)與重繪(Repaint)。