徹底瞭解渲染引擎以及幾點關於效能優化的建議

墨箏發表於2018-04-15

前言

在日常開發過程中,要編寫效能足夠優秀的程式碼,構造更加穩定的應用,我們不僅要對javascript本身的執行機制有深入的瞭解,更要對其宿主環境有更加深刻的認識,理解其工作原理以及組成結構,它可以幫助我們對web世界的運轉模式有更高層級的認知。這次想要介紹的是瀏覽器的渲染引擎。

瀏覽器的構成

在具體介紹渲染引擎之前,我們先來看看瀏覽器的構成,看看渲染引擎在瀏覽器中扮演的是一個怎樣的角色。關於瀏覽器的構成可以參見下圖:

徹底瞭解渲染引擎以及幾點關於效能優化的建議

如上圖所示,瀏覽器從外到內的組成包括了以下幾個部分:

  1. User interface,即瀏覽器的視覺外觀,具體包括其地址輸入欄,前進後退鍵,書籤選單欄等等。
  2. Browser engine, 瀏覽器引擎,它主要處理User interface與render engine,即渲染引擎之間的互動。
  3. Rendering engine, 即本文介紹的重點-渲染引擎,它負責解析html以及css,並將解析後的內容渲染到螢幕上,完成web頁面的展示。
  4. Networking, 瀏覽器的網路處理層,主要負責處理xhr之類的網路請求,之後我也會專門寫一篇文章來詳細介紹它。
  5. Javascript engine, 負責javascript的執行時處理,關於它我之前已經專門從記憶體管理和非同步執行方面寫了兩篇文章,沒有看過的可以參見我的專欄哦。
  6. Data persistence,資料持久化,即瀏覽器的本地資料儲存,目前瀏覽器所支援的幾種本地資料儲存方式包括有localstorage,indexDB,webSQL以及FileSystem。
    瞭解了渲染引擎在整個瀏覽器中的角色作用後,我們回到渲染引擎本身,看看它是如何完成頁面渲染的。

渲染過程

渲染引擎接收到網路層傳遞過來的頁面文件內容後,大致的解析處理過程如下:

徹底瞭解渲染引擎以及幾點關於效能優化的建議

dom樹構造

首先解析html來構成dom樹,假設有如下html文件內容:

<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="theme.css">
  </head>
  <body>
    <p> Hello, <span> friend! </span> </p>
    <div> 
      <img src="smiley.gif" alt="Smiley face" height="42" width="42">
    </div>
  </body>
</html>
複製程式碼

解析後,其dom樹構造示意圖如下:

徹底瞭解渲染引擎以及幾點關於效能優化的建議

可以看出dom樹中每個節點的父子關係與html元素的父子關係保持一致。
dom樹構造完成後還不能直接生成render tree,還需要cssom樹的配合。

cssom樹

cssom是指css object model,當瀏覽器在解析html時如果在head中遇到了連線到外部css檔案的link標籤,瀏覽器就會立刻發起請求獲取該css檔案的內容,需要注意的是css檔案的獲取和解析不會阻塞html的解析,但是script 標籤的內容無論是下載還是執行都會阻塞html解析。假設頁面中的css內容如下:

body { 
  font-size: 16px;
}

p { 
  font-weight: bold; 
}

span { 
  color: red; 
}

p span { 
  display: none; 
}

img { 
  float: right; 
}
複製程式碼

瀏覽器會將其轉換成如下的cssom樹:

徹底瞭解渲染引擎以及幾點關於效能優化的建議

也許你會奇怪為什麼css也會有這樣的樹形結構,這是因為瀏覽器在為某個dom物件計算最終的樣式規則時,是先從最一般的規則開始,然後才是具體指定的規則。比如上述示例中,對於span標籤,會先新增body中的font-size為16px的規則,然後才是它自己定義的規則,如果span標籤包裹於某個p標籤下還會新增display為none的規則,總結下來就是先apply父級規則,然後apply specific rule.

render樹

上述兩項工作完成後,通過dom樹與cssom樹的結合就可以生成render樹,或許你會問render樹到底是什麼?為什麼一定要先生成render樹,而不能直接用dom樹和cssdom樹去做paint呢?有這樣的疑問很好,所謂render樹,它其實是擁有樣式表現的可見元素按照其文件順序構造而成的樹形結構,生成它的目的是確保元素的渲染過程是嚴格按照文件流順序以及樣式規則進行的。rener tree的示意圖如下:

徹底瞭解渲染引擎以及幾點關於效能優化的建議

layout

render樹雖然構造完成,但是其中的節點還需要進行位置和尺寸的計算,這些數值的計算過程就是layout.
layout是一個遞迴的過程,它從根元素也就是html元素開始計算,位置計算的座標系也是相對於根元素,html元素座標為(0,0).後續的計算可能是區域性更新也有可能是整體替換。 layout過程結束後就意味著每個節點都會獲得它將在螢幕上展示的位置座標,可以開始進行真正的渲染過程了。

painting

在這個階段,瀏覽器就會把整個文件結構展示在頁面上,與layout一樣,painting也有區域性更新和全部更新兩種可能。這取決於你的dom操作機制。 painting是一個漸進的過程,為了更好的UX體驗,渲染引擎不會等到所有html全部解析完成後才開始,而是先解析完成的部分先繪製,其餘部分解析完成後再行繪製。
至此渲染引擎的整個執行流程已經結束,瞭解了渲染引擎的執行機制,下面我們就來看看可以從哪些方面入手去做頁面的優化,以獲得更好的使用者體驗。

關於效能優化

從渲染引擎的角度,我們可以從一下五個方面入手去做效能優化.

  1. javascript, 在js程式碼編寫過程中我們需要更多的注意會引起視覺變化的操作,比如dom操作等,尤其是在單頁應用中,這樣的場景更加常見。關於javascript方面的優化,我的建議是:
  • 避免使用setTimeout或者setInterval這類定時器去操作視覺更新,因為它們的執行機制並不精準,有可能會離我們想要的時機相去甚遠。
  • 將計算量大的操作交給web workers,因為js的執行會阻塞頁面的更新以及對使用者互動的響應.
  • 如果需要非同步的操作dom,那麼請選擇用microtask的方式,比如mutationObserver。
  1. css,在css編寫過程中要儘量減少選擇器的複雜度,相比給某個元素確定其樣式規則,元素選擇器的計算要多消耗50%的時間。
  2. layout, 在layout過程中瀏覽器需要確定每個元素的座標和尺寸,這意味著layout是一個計算密集型的過程,所以我們需要儘量減少重複觸發layout。針對layout,我的優化建議是:
  • 減少對元素位置和尺寸有影響的屬性的操作,比如width,height,left,top等等,這些操作會使瀏覽器重新進行layout.
  • 儘可能使用flexbox進行佈局,它比傳統的基於盒模型的佈局有更好的效能優勢。
  • 避免強制觸發layout,瀏覽器對於dom操作和屬性的變化是有原生優化機制的,它會等到合適的時機將多個操作集中執行以避免高頻觸發layout,但如果你在操作或更新了某個dom之後立即訪問它的某些屬性,比如offsetHeight這些,它就會立刻觸發layout,我們要儘量避免這樣的訪問。

總結

這篇文章主要介紹了瀏覽器渲染引擎的執行機制,相對來說是一篇非常偏基礎知識的文章,也是lz我最近對前端基礎重新梳理回顧對一次思考總結,希望也會對你有幫助。

相關文章