瀏覽器加載解析渲染網頁原理

  • 瀏覽器加載網頁資源的原理
  • JS與CSS阻塞
  • 重排與重繪

 1、瀏覽器加載網頁資源的原理

一、HTML支持的組要資源類型javascript

在瀏覽器內核有一個管理資源的對象CachedResource類,在CachedResource類下有不少子類來分工不一樣的資源管理,這些資源管理子類分別是:css

 資源  資源管理類
 HTML  MainResource ===> CachedRawResource
 JavaScript  CachedScript
 CSS  CachedCSStyleSheet
 圖片  CachedImage
 SVG  CachedSVGDocument
 CSS Shader  CachedShader
 視頻、音頻、字幕  CachedTextTrack
 字體文件  CachedFont
 XSL樣式表  CachedXSLStyleSheet

二、資源緩存html

資源的緩存機制是提升資源使用效率的有效方法。基本思想就是創建一個資源緩存池,當web須要請求資源時,會先從資源池中查找是否存在相應的資源,若是有的話就直接取緩存,若是沒有就建立一個新的CachedResource子類的對象,併發送請求給服務器(由網絡模塊完成),請求回來的資源會被添加到資源池,而且將資源(數據信息:好比在資源池中的物理地址)設置到該資源的對象中去,以便下次使用。vue

下面是一個縮減版的資源請求原理圖:java

實質上的操做是在資源對象中找到對應資源的物理地址(url),而後返回給渲染引擎,渲染引擎在渲染頁面時根據url獲取物理內存中的資源數據。因爲資源的惟一特性是url,因此當兩個資源有不一樣的url,可是他們的內容徹底相同時,也不會被認定是同一個資源。web

注:這裏所說的緩存是內存,不是磁盤。算法

三、資源加載器express

在WebKit中共有三種類型的資源加載器,分別是:bootstrap

3.1針對每種資源類型的特定加載器,用來加載某一類資源。例如「image」這個元素,該元素須要圖片資源,對應的頂資源加載器是ImageLoader類。瀏覽器

3.2資源緩存機制的資源加載器,特色是全部特定加載器都共享它來查找並插入緩存資源——CachedResourceLoader類。特定加載器是經過緩存機制的資源加載器來查找是否有緩存資源,它屬於HTML的文檔對象。

3.3通用的資源加載器——ResourceLoader類,是在WebKit須要從網絡或者文件系統獲取資源的時候使用該類只負責得到資源的數據,所以被全部特定資源加載器所共享,它屬於CachedResource類,與CachedResourceLoader類沒有繼承關係。

若是說資源緩存和網絡資源是瀏覽器要渲染頁面的資源實體,那資源加載器就是爲瀏覽器實現頁面渲染提供資源數據的搬運工。前面的資源請求至關於就是資源地址尋址的過程,真正爲渲染提供資源的過程是下面這樣的:

這個資源加載看起來很複雜,可是模塊分工很明確,基於資源對象與內存資源緩存的對應關係(每一個緩存資源在資源對象上有一個實例),當瀏覽器觸發資源請求時先經過判斷資源是否有緩存資源,若是有的話就就直接拿緩存資源給渲染引擎,若是沒有就經過網絡請求獲取資源給渲染引擎,而且同時會將資源緩存到內存中。

同CachedResourceLoader對象同樣,資源池也屬於HTML文檔對象,因此資源池不能無限大,對於資源容量不能無限大的問題瀏覽器的解決方法有兩種:第一種是採用LRU(Least Recent Rsed最近最少使用原則)算法。第二種方法是經過HTTP協議決定是否緩存,緩存多久,以及何時更新緩存,而後咱們開發時還可決定資源如何拆分,拆分可讓我決定哪些資源緩存,哪些資源不緩存。

當請求協議指定能夠取緩存數據,請求資源會先判斷內存中是否有資源,而後將資源的信息(版本,緩存時常等)經過HTTP報文一塊兒發送給服務器,服務器經過報文判斷緩存的資源是不是最新的,資源緩存是否超時來決定是否從新獲取服務端的資源,若是不須要從新獲取服務端的資源,服務器會返回狀態碼304,告訴瀏覽器取本地緩存資源。

下面經過Chrome瀏覽器來請求餓了嗎官網,在控制檯查看數據請求的資源加載過程,而且經過刷新頁面查看當頁面刷新時瀏覽器在緩存中取了哪些信息:

接着咱們再來刷新頁面看看取了哪些緩存數據:

 

能夠看到餓了嗎官網的緩存機制是將document主文件和js文件作了緩存處理。這樣的處理方式能夠很大程度上提升頁面性能和下降服務器請求壓力,至於爲何就是接下來的內容了。

 2、解析HTML標籤和CSS樣式表、生成DOMTree和CSSTree

前面介紹了瀏覽器資源請求與資源加載的基本原理,看上去好像是一個簡單的線性步驟,可是實質上瀏覽器內部是多進程異步加載這些資源的,咱們知道網頁的效果是基於DOM結構和CSS樣式表來完成基本的頁面效果呈現,可是JS代碼又能夠對DOM節點進行增刪該查操做,還能夠修改DOM的CSS樣式,那必然就是須要先有DOM結構,而後添加CSS樣式,再就這兩個資源的基礎經過JS修改後才能呈現出來,可是何時加載(指的是下載資源,並非前面的資源加載到頁面上的整個過程)?何時執行?何時渲染頁面?按照什麼規則來完成這些工做呢。

一般咱們給某個服務器發送一個web請求時,首先返回的是一個HTML資源。假設這個資源的內部代碼以下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title></title>
    <link rel="stylesheet" type="text/css" href=".../css/xxx.css">
</head>
<body>
    <div>
        <p>
            <span></span>
        </p>
        <ul>
            <li><img src=".../image/xxx.png" alt=""></li>
            <li><img src=".../image/xxx.png" alt=""></li>
            <li><img src=".../image/xxx.png" alt=""></li>
        </ul>
    </div>
    <script src=".../javascripts/xxx.js" type="text/javascript"></script>
</body>
</html>

本地獲取到了HTML資源後第一步就是解析HTML,也就是常說的DOM解析,首先是建立一個document對象,而後經過DOM解析出每一個節點。經過DOM解析發現頁面中有css外部樣式表須要加載,就當即經過CSS加載器執行加載。解析到img元素髮現須要加載圖片,就當即經過圖片加載器執行加載,這個過程不會等待前面加載的資源加載完成才啓動第二個加載,而是經過異步的方法開啓多個加載線程,而且瀏覽器底層會開啓多個進程來處理這些線程(Chrome會開啓五個進程)。一樣解析到了script元素時發現須要外部js資源會當即加載js文件資源。

深度優先原則解析構建DOM樹和CSS樹:

深度優先原則就是對每個結構順着第一個內部節點一直往內部解析,直到結構盡頭,而後再回退到上一個節點,再對第二個節點執行深刻優先原則的解析構建。下圖是上面示例請求到的HTML資源的解析流程圖:

 

按照示例HTML解析流程圖,根據編號順序按照1-->1.1-->1.2-->1.3-->1.4-->2-->2.1-->2.1.1-->2.1.1.1-->2.1.2-->2.1.2.-->2.1.2.1-->2.1.2.2-->2.1.2.3-->2.2。用一句來表達這種解析原則就是一條道走到黑,開玩笑,可是的確很形象哈。CSS樣式表解析和構建CSS樹也一樣使用這個原則。當DOMTree和CSSTree都構建完成之後就會被合併成渲染樹(randerTree)。渲染樹解析完畢之後就開始繪製頁面。

 3、JS與CSS阻塞

瞭解了DOMTree和CSSTree的構建原理,而後合成randerTree繪製頁面,可是這個過程怎麼能缺乏JS呢?有了JS的參與,這個過程就會變得複雜了。首先,CSS資源是異步加載(下載),在CSS資源加載的過程當中,DOM解析會繼續執行操做。可是當遇到script標籤的時候,若是是外部資源就要當即加載(下載),若是是內部資源就會當即執行JS代碼,當即執行JS代碼會阻斷HTML的解析(由於JS會操做DOM節點增刪改查什麼的,還會操做元素樣式),霸道總裁JS就這樣讓傻媳婦HTML傻呆着讓它隨心所欲了。就算是外部JS資源加載(下載)的過程HTML的解析也是被阻斷的,這個過程是必須等到JS加載(下載)完,而後還要等他執行完才能繼續解析HTML。

<img class="img1" src="https://img.baidu.com/search/img/baidulogo_clarity_80_29.gif" alt="Baidu" align="bottom" border="0">
<script type="text/javascript">

    // 循環5秒鐘
    var n =Number(new Date());
    var n2 = Number(new Date());
    while((n2 - n) < (10*1000)){
        n2 = Number(new Date());
    }
    console.log(document.querySelectorAll(".img1"));//NodeList [img.img1]
    console.log(document.querySelectorAll(".img2"));//NodeList []
</script>
<img class="img2" src="https://gss1.bdstatic.com/9vo3dSag_xI4khGkpoWK1HF6hhy/baike/w%3D268%3Bg%3D0/sign=7aa2c00bdd58ccbf1bbcb23c21e3db03/908fa0ec08fa513defeb0567316d55fbb3fbd9c2.jpg">
<script>
    var n3 = Number(new Date() - n2);
    console.log(n3);//13
    console.log(document.querySelectorAll(".img1"));//NodeList [img.img1]
    console.log(document.querySelectorAll(".img2"));//NodeList [img.img2]
</script>

由上面的示例能夠說明js執行會阻塞DOMTree構建,否則在JS等待的10秒裏足夠解析一個img元素,可是10秒後只能查詢到img1,img2查詢不到(打印空DOM節點對象)。當第二次打印的時候兩個img節點就都獲取到了。接着咱們來看看外部JS加載會不會阻塞DOMTree構建:

<script>
    var n =Number(new Date());
</script>
<!-- 設置網速30kb/s測試js是否阻塞渲染 -->
<script src="https://cdn.staticfile.org//vue/2.2.2//vue.min.js"></script>
<script>
    var n3 = Number(new Date() - n);
    console.log(n3);//30~40秒 ---- 註釋外部js加載代碼測試時間差爲0秒
</script>

測試結果是外部JS的加載也會阻塞HTML解析構建DOMTree。因此結論是JS的加載和執行都會阻塞DOMTree的構建,接着問題又來了,咱們前面提到過JS代碼會操做DOM還會操做CSS,因此從理論上講JS確定得須要等到CSS加載解析完纔會執行,CSS阻塞JS執行是確定的,再思考CSS的加載(下載)會阻塞JS的加載(下載)嗎?

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link type="text/css" rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
    <script src="https://cdn.staticfile.org//vue/2.2.2//vue.min.js" type="text/javascript" charset="utf-8" async defer></script>
</head>
<body>
</body>
</html>

咱們來看Chrome控制檯的時間線:

由Chrome控制檯的時間線能夠看到外部JS和外部CSS幾乎是同時開始加載,CSS加載並無阻塞JS的加載。既然這樣咱們再來測試如下CSS加載阻塞JS執行是不是真的?

<script>
    var n = Number(new Date());
</script>
<link type="text/css" rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
<script>
    console.log(Number(new Date()) - n);//外部CSS阻塞JS執行40~200毫秒 --- 註釋外部CSS代碼測試差值0~1毫秒
</script>

可能有人會疑惑我爲何不測試外部CSS會不會阻塞HTML解析,你想一想若是CSS阻塞HTML解析那JS加載必須會被阻塞吧,因此CSS加載也就不會阻塞HTML解析了。可是,CSS會阻塞JS執行,也就間接的阻塞了JS後面的DOM解析。

其實相對來講JS與CSS阻塞仍是比較好理解的,畢竟還有可參考的數值和可視的圖像信息,接下來的問題就只能依靠邏輯推理了。

 4、JS時間線

在闡述JS時間線以前,我另外總結了一部分很是重要的內容:JS的異步加載(JS異步加載的三種方案),JS異步加載與下面的內容相關聯的內容比較多,建議在瞭解下面內容以前先了解一下JS異步加載。

在前面的內容中解析了訪問網站獲取資源的基本原理,而後資源被訪問到本地後怎麼解析,解析時發什麼的異步資源加載,同步資源加載,同步執行等一系列內容。而後在JS異步加載中提到了script.onload事件、script.onreadystatechange事件、script.readyState狀態,而後還有document.readyState="interactive"文檔狀態和docuement.readyState="complete"文檔狀態。這些內容都發生在打開網頁的那一瞬間,可是這一瞬間不僅是檢驗物理配置的性能、瀏覽器內核的性能以及網絡的性能,還關係到web開發者基於這些已定的基礎平臺的代碼優化,因此咱們有必要對這整個過程有很是清晰的理解,才能實現友好的程序設計。下面咱們就經過JS時間線來描述這個過程如何發生的:

頁面加載的五個步驟和JS時間線的十個環節:

五個步驟:

  • 解析HTML生成DOMTree
  • 解釋CSS樣式表生成CSSTree
  • 合併DOMTree和CSSTree生成randerTree
  • randerTree生成完之後開始繪製頁面
  • 瀏覽器在解析頁面時同時下載頁面內容內容數據(異步:圖片,src)

JS時間線之十個環節:

 要說是JS時間線的話,可能不是很恰當,或者應該說是文檔模型初始化構建過程的JS表示,可以操做DOM對象接口的語言有不少,這裏就是用JS來表示DOM對象模型初始化的整個過程。

  • 1.建立document對象,開始解析web頁面。解析HTML原始和他們的文件內容添加Element對象和Text節點到文檔中。階段:document.readyState = "loading"。(表示能夠觸發一次document.onreadystatechange事件)
  • 2.遇到link外部css,建立線程加載,並繼續解析文檔。
  • 3.遇到script外部JS,並無設置async、defer,瀏覽器加載,並阻塞,等待JS加載完成並執行腳本,完後繼續解析文檔。
  • 4.遇到script外部JS,而且設置async、defer,瀏覽器建立線程加載,並繼續解析文檔。
  • 5.遇到img等外部內容數據,先正常解析DOM結構,而後瀏覽器異步加載src,而且繼續解析文檔。
  • 6.當文檔解析完成後。document.readyState = "interactive"。(表示能夠觸發一次document.onreadystatechange事件
  • 7.文檔解析完成後,全部設置有defer的腳本會按照順序執行。(禁止使用document.wrlte())。
  • 8.document對象觸發DOMContentLoaded事件,這也標誌着程序執行從同步執行階段,轉化爲事件取動階段。(這裏開始繪製頁面)
  • 9.當全部async的腳本加載完成,img等加載完成後,document.readyState = "complete",window對象觸發事件。(表示能夠觸發一次document.onreadystatechange事件或者標準瀏覽器能夠觸發window.onload事件了)
  • 10.今後,以異步響應方式處理用戶輸入、網絡事件等。
//readyState屬性返回當前文檔的狀態
uninitialized - 還未開始載入
loading - 載入中
interactive - 已加載,文檔與用戶能夠開始交互
complete - 載入完成--loaded

 5、重排/迴流與重繪

關於重排/迴流(reflow)重繪(repaint)簡單來講就是會將已經計算好的佈局和構建好的渲染樹(randerTree)從新計算和構建所有或者部分。這部分發生在DOMTree和CSSTree解析完成之後,也就是會發生在構建randerTree時和以後,這裏咱們重點關注發生在randerTree構建時的重排/迴流和重繪問題,也是網頁渲染除了JS、CSS阻塞以後的性能優化區間。

發生重排/迴流與重繪其本質上從新佈局和構建randerTree,若是將DOM以前的執行過程理解爲同步,這個時候瀏覽器轉爲事件取動的異步階段,瀏覽器內核在構建randerTree的同時JS也會被事件取動參與修改文檔的結構和樣式,也是觸發重排/迴流與重繪行爲的關鍵所在,而本質上作的事情就是從新計算佈局和構建randerTree樹,因此在解析重排與重繪以前先來了解如下佈局計算和randerTree構建:

 佈局

在構建randerTree時並不會把CSS樣式表或者行內樣式表示元素大小和位置的數據添加到RanderObject上,而是要基於樣式設置(如):width、height、font-size、display、left、top、bottun、right還有borde、padding、margin的大小,結合上下文的相互做用(好比有子元素自適應父級元素大小和位置或者父元素基於子元素定義自身大小和位置),最後使用RanderObject上的layout()方法計算出肯定的元素大小和位置,這個過程layout()方法是遞歸完成整個計算操做。

由於佈局計算須要基於元素上下節點來進行,元素的大小和位置變化都有可能會影響到父級和子級的元素大小和位置變化,因此randerTree上的某個RanderObject的相關數據發生變化除了自身的layout()方法須要從新執行計算,還可能會觸發上下級的節點的layout()方法的從新執行計算。

因此當構建randerTree的時候由document.onreadystatechange事件、defer的腳本、DOMContentLoaded事件還有不肯定的src異步加載的JS腳本均可能在這時候修改元素的大小和位置,甚至修改DOM結構。

除了腳本的影響外,還有多是瀏覽器窗口發生產生變化致使全局的randerTree從新佈局計算,另外若是腳本修改了全局的樣式也一樣可能會觸發全局的從新佈局計算。

 重排/迴流(reflow)

有了前面對佈局的介紹,重排/迴流就一目瞭然了,當因爲腳本執行或者瀏覽器窗口變化,引起RanderObject上的layout()方法從新計算機佈局數據,就叫作重排/迴流。從字面上的含義來理解重排很容易,就是因爲元素的大小和位置變化頁面從新排列布局。迴流就存在一些邏輯上的理解了,在佈局中由於元素節點的位置和大小是存在上下級和同級之間相互影響的,因此若是有腳本修改DOM節點或者大小位置樣式,就會對相關連的元素進行判斷查找修改的範圍指定修改邏輯,制定layout()方法的遞歸順序的最優方案,這個查詢判斷和修改過程就是須要在節點之間來回操做,這也就是迴流。實質上重排/迴流說的都是一回事。

 重繪(repaint)

重繪不會影響佈局,可是當腳本觸發了樣式修改,而修改的部分是背景(圖片和顏色)、字體顏色、邊框顏色等,而這些修改也存在嵌套的節點鏈級相互影響,因此也是須要遍歷操做,重繪不至於影響到佈局,但也是一個相對損耗性能的操做,畢竟都須要DOM文檔和JS引擎結構之間的橋樑通道來執行操做。不太重繪相對於重排來講就要快的多了。

重排/迴流與重繪是會發生在randerTree構造時,也會發生在randerTree構造結束後,都是相對損耗CPU甚至GPU的操做,只是頁面首次渲染更值得的咱們關注。

 繪製(paint)

 

當randerTree構建完成之後就會開始繪製頁面了,在繪製頁面過程當中仍然可能發生重排與重繪,但這裏須要重點關注的是圖層合併,繪製主要是基於CPU的計算來實現,同時瀏覽器基本上都採用GPU加速的混合模式,其實瀏覽器自己不須要操做圖層合併,由於繪圖無論是CPU仍是GPU來實現都是基於元素的大小和位置將它們實現的圖層,圖們自己就在同一個位置,因此無需合併操做。

CPU主要負責randerTree的繪製工做,它與GPU的配合在不一樣瀏覽器內核中會略微不一樣,可是在同一個位置出現的圖層越多,確定是對性能的損耗就越大。並且因爲CPU主要負責randerTree的繪製,多圖層就會對GPU帶來很大的工做負載,具體包括:CSS3 3D變形、CSS3 3D 變換、WebGL 和 視頻。也有浮動,定位,溢出隱藏,z座標重疊等都是在繪製過程當中比較損耗性能的行爲。

最後通過這樣艱難的過程事後,網頁終於呈如今咱們桌面,可是注意window事件交互不會等待繪製完成,決定window事件交互的是資源是否所有加載完成,這裏指的資源是HTML文檔包含內容資源,並不包含外部腳本加載的資源。

(減小重排與重繪的一些要點)

 1 1:不要經過父級來改變子元素樣式,最好直接改變子元素樣式,改變子元素樣式儘量不要影響父元素和兄弟元素的大小和尺寸
 2 2:儘可能經過class來設計元素樣式,切忌用style
 3 3:實現元素的動畫,對於常常要進行迴流的組件,要抽離出來,它的position屬性應當設爲fixed或absolute
 4 4:權衡速度的平滑。好比實現一個動畫,以1個像素爲單位移動這樣最平滑,但reflow就會過於頻繁,CPU很快就會被徹底佔用。若是以3個像素爲單位移動就會好不少。
 5 5:不要用tables佈局的另外一個緣由就是tables中某個元素一旦觸發reflow就會致使table裏全部的其它元素reflow。在適合用table的場合,能夠設置table-layout爲auto或fixed,
 6 6:這樣可讓table一行一行的渲染,這種作法也是爲了限制reflow的影響範圍。
 7 7:css裏不要有表達式expression
 8 8:減小沒必要要的 DOM 層級(DOM depth)。改變 DOM 樹中的一級會致使全部層級的改變,上至根部,下至被改變節點的子節點。這致使大量時間耗費在執行 reflow 上面。
 9 9:避免沒必要要的複雜的 CSS 選擇器,尤爲是後代選擇器(descendant selectors),由於爲了匹配選擇器將耗費更多的 CPU。
10 10: 儘可能不要過多的頻繁的去增長,修改,刪除元素,由於這可能會頻繁的致使頁面reflow,能夠先把該dom節點抽離到內存中進行復雜的操做而後再display到頁面上。
11 11:請求以下值offsetTop, offsetLeft, offsetWidth, offsetHeight,scrollTop/Left/Width/Height,clientTop/Left/Width/Height,瀏覽器會發生reflow,建議將他們合併到一塊兒操做,能夠減小回流的次數。
View Code
相關文章
相關標籤/搜索