根據瀏覽器渲染界面原理理解渲染阻塞、瀏覽器的重繪(repaints)與迴流(reflows)

前面有講到當用戶在瀏覽器輸入url以後,通過一系列的過程,會最終向服務器請求到文檔數據,文檔數據請求到以後,瀏覽器會將這些數據傳給瀏覽器渲染引擎,渲染引擎開始正式工做了。javascript

構建dom樹,解析css

首先瀏覽器接收到html文檔,就會把HTML在內存中轉換成DOM樹,HTML中的每一個tag都是DOM樹中的1個節點,根節點就是咱們經常使用的document對象。DOM樹裏包含了全部HTML標籤,包括display:none隱藏,還有用JS動態添加的元素等。在轉換的過程當中若是發現某個節點(node)上引用了CSS或者 image,就會再次向服務器請求css或image,而後繼續執行構建dom樹的轉換,而不須要等待請求的返回,當請求的css文件返回後,就會開始解析css style,瀏覽器把全部樣式(用戶定義的CSS和用戶代理)解析成樣式結構體,在解析的過程當中會去掉瀏覽器不能識別的樣式,好比IE會去掉-moz開頭的樣式,而FF會去掉_開頭的樣式。css


構建render Tree及繪製

DOM Tree 和樣式結構體組合後構建render tree,也就是渲染樹。渲染樹和dom樹有很大的區別,render tree中每一個NODE都有本身的style,並且 render tree不包含隱藏的節點 (好比display:none的節點,還有head節點),由於這些節點不會用於呈現,並且不會影響呈現的,因此就不會包含到 render tree中。注意 visibility:hidden隱藏的元素仍是會包含到 render tree中的,由於visibility:hidden 會影響佈局(layout),會佔有空間。根據CSS2的標準,render tree中的每一個節點都稱爲Box (Box dimensions),理解頁面元素爲一個具備填充、邊距、邊框和位置的盒子。一旦render tree構建完畢後,瀏覽器就能夠根據render tree來繪製頁面了。html

注意:因爲瀏覽器的流佈局,對渲染樹的計算一般只須要遍歷一次就能夠完成。但 table及其內部元素除外,它可能須要屢次計算才能肯定好其在渲染樹中節點的屬性,一般要花3倍於同等元素的時間。這也是爲何咱們要避免使用 table作佈局的一個緣由。java


渲染阻塞

在瀏覽器進行加載時,實際上是並行加載全部資源。對於css和圖片等資源,瀏覽器加載是異步的,並不會影響到後續的加載、html解析和後續渲染。node

css阻塞渲染

由上面過程能夠看到,頁面佈局是在渲染樹構建好以後發生的,而渲染樹依賴css樣式結構體,因此CSS 被視爲阻塞渲染的資源但不阻塞html的解析,不會阻塞dom樹的構建),這意味着瀏覽器將不會渲染任何已處理的內容,直至 CSSOM 構建完畢。jquery

由於css會阻塞渲染,因此咱們應該儘早的儘快地下載到客戶端,以便縮短首次渲染的時間。平時在開發的時候,應注意如下幾點:chrome

  • 將CSS放在head,無論內聯仍是外聯都儘早開始下載或者構建CSSOM(前提是這個CSS是首屏必須的)
  • 避免使用CSS import,CSS中能夠用import將另外一個樣式表引入,不過這樣會在構建CSSOM時會增長一次網絡來回時間。
  • 適度內聯CSS,衡量其餘因素,如外聯,看網絡來回影響多大,考慮css文件的大小
  • 全面考慮渲染狀況,網速差、文件下載失敗等,防止白屏時間太長

同時,還有如下優化點:瀏覽器

1、媒體查詢緩存

經過使用媒體查詢,咱們能夠根據特定的需求(好比顯示或打印),也能夠根據動態狀況(好比屏幕方向變化、尺寸調整事件等)定製外觀,服務器

<link href="style.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 40em)">

看上面的代碼,
第一行,這樣的普通聲明,會阻塞渲染
第二行,這個聲明,只在打印網頁時應用,所以網頁在瀏覽器中加載時,不會阻塞渲染。
第三行,提供了由瀏覽器執行的「媒體查詢」,只有符合條件時,樣式表會生效,瀏覽器纔會阻塞渲染,直至樣式表下載並處理完畢。

2、preload

<link rel="preload" href="index_print.css" as="style" onload="this.rel='stylesheet'">

preload是resoure hint規範中定義的一個功能,顧名思義預加載,將rel改成preload後,至關於加了一個標誌位,瀏覽器解析的時候會提早創建鏈接或加載資源,作到儘早並行下載,而後在onload事件響應後將link的rel屬性改成stylesheet便可進行解析。

IE chrome firefox三者的差別

  1. IE 只要看到HTML 標籤就會進行繪製
  2. chrome 無論css放在前面仍是後面,都要等到CSSOM構建造成後纔會繪製到頁面上
  3. firefox 放在head則會阻塞繪製,放在body末尾會先繪製前面的標籤

3、動態添加link

var style = document.createElement('link');
style.rel = 'stylesheet';
style.href = 'index.css';
document.head.appendChild(style);

js動態添加DOM元素link,不會阻塞渲染。
loadCSS.js,CSS preload polyfill第三方庫,原理同上

4、代碼簡練


js阻塞

js可能會操做html,css,因爲瀏覽器不瞭解腳本計劃在頁面上執行什麼操做,它會做最壞的假設並阻止解析器,也就是以前講過瀏覽器的GUI線程與js引擎線程是互斥的。因此,js會阻塞渲染

瀏覽器對於js腳本文件的加載,則會致使html解析和渲染中止,直至js腳本加載並執行完畢才繼續,可是對於後續的非js資源加載並不會中止,瀏覽器會對後續資源進行預加載。而資源加載是屬於另外單獨的線程,因此js加載並不會影響其餘非js資源的加載,是瀏覽器的機制。

總的來講就是如下幾點:

  • js腳本在文檔中的位置很重要,由於其跟html和css有很強的依賴關係
  • 在HTML解析器解析到script標籤後,會中止DOM構建
  • javascript能夠操做DOM和CSSOM,但進行這些行爲時要確保相應DOM和CSSOM已經存在,
  • JavaScript 執行將暫停,直至 CSSOM 就緒

當CSS後面跟着嵌入的JS的時候,該CSS就會出現阻塞後面資源下載的狀況,由於瀏覽器會維持html中css和js的順序,樣式表必須在嵌入的JS執行前先加載、解析完。而嵌入的JS會阻塞後面的資源加載,因此就會出現CSS阻塞下載的狀況。

使用chrome瀏覽器的performance工具查看瀏覽器的渲染過程:

例以下面這段代碼,看瀏覽器是如何一步步將界面繪製出來

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="author" content="Reddy.Huang, i@0u0b.com"/>
    <title>瀏覽器渲染</title>
    <link href="./css/main.css" rel="stylesheet">

</head>
<body>
    <div class="wrap">
        <div class="left">
        </div>
        <div class="middle">
            <div class="line">

            </div>

        </div>
        <div class="right">
            <p>fgdgg</p>
            <p>fgdgg</p>
            <p>fgdgg</p>
            <p>fgdgg</p>
            <p>fgdgg</p>
        </div>
    </div>

    <script src="./js/3.js"></script>

</body>
</html>

clipboard.png

clipboard.png

經過瀏覽器的工具上的能夠很清楚的看到界面的渲染過程,也能夠很清楚的看到請求加載資源的時候,不會對html解析形成影響,但若是資源加載過慢,會致使渲染阻塞,經過此圖能夠很好的理解瀏覽器的渲染機制

若是我把js放在css以後,以下代碼:

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="author" content="Reddy.Huang, i@0u0b.com"/>
    <title>瀏覽器渲染</title>
    <link href="./css/main.css" rel="stylesheet">
    <script src="./js/3.js"></script>

</head>
<body>
    <div class="wrap">
        <div class="left">
        </div>
        <div class="middle">
            <div class="line">

            </div>

        </div>
        <div class="right">
            <p>fgdgg</p>
            <p>fgdgg</p>
            <p>fgdgg</p>
            <p>fgdgg</p>
            <p>fgdgg</p>
        </div>
    </div>

   

</body>
</html>

再次查看瀏覽器的渲染過程:

clipboard.png

圖中能夠明顯的看出,首先瀏覽器開始解析html,而後再解析的過程當中遇到css,開始加載css資源,遇到js開始加載js資源,當css加載完成後,開始解析css,js加載完成後,則開始解析js,此時解析html生成dom樹會中止,直到js解析完成以後,纔再次開始解析html,從新計算樣式,佈局,生成渲染樹,最終纔是界面繪製,因此在開發的時候不要將js文件寫在頭部,這樣會影響界面的繪製,致使界面出現空白

瀏覽器的重繪(repaints)與迴流(reflows)

重繪
當render tree中的一些元素須要更新屬性,而這些屬性只是影響元素的外觀,風格,而不會影響佈局的,好比background-color。則就叫稱爲重繪。

迴流
當render tree中的一部分(或所有)由於元素的規模尺寸,佈局,隱藏等改變而須要從新構建。這就稱爲迴流(reflow)。
每一個頁面至少須要一次迴流,就是在頁面第一次加載的時候。在迴流的時候,瀏覽器會使渲染樹中受到影響的部分失效,並從新構造這部分渲染樹,完成迴流後,瀏覽器會從新繪製受影響的部分到屏幕中,該過程成爲重繪。

迴流必然會形成重繪,重繪不會形成迴流。

迴流什麼時候發生:

當頁面佈局和幾何屬性改變時就須要迴流。下述狀況會發生瀏覽器迴流:

一、添加或者刪除可見的DOM元素;

二、元素位置改變;

三、元素尺寸改變——邊距、填充、邊框、寬度和高度

四、內容改變——好比文本改變或者圖片大小改變而引發的計算值寬度和高度改變;

五、頁面渲染初始化;

六、瀏覽器窗口尺寸改變——resize事件發生時;

迴流比重繪的代價要更高,迴流的花銷跟render tree有多少節點須要從新構建有關係,假設你直接操做body,好比在body最前面插入1個元素,會致使整個render tree迴流,這樣代價固然會比較高,但若是是指body後面插入1個元素,則不會影響前面元素的迴流。

若是每句JS操做都去迴流重繪的話,瀏覽器可能就會受不了。因此不少瀏覽器都會優化這些操做,瀏覽器會維護1個隊列,把全部會引發迴流、重繪的操做放入這個隊列,等隊列中的操做到了必定的數量或者到了必定的時間間隔,瀏覽器就會flush隊列,進行一個批處理。這樣就會讓屢次的迴流、重繪變成一次迴流重繪。

雖然有了瀏覽器的優化,但有時候咱們寫的一些代碼可能會強制瀏覽器提早flush隊列,這樣瀏覽器的優化可能就起不到做用了。當你請求向瀏覽器請求一些 style信息的時候,就會讓瀏覽器flush隊列,好比:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • width,height
  • 請求了getComputedStyle(), 或者 IE的 currentStyle

當請求上面的一些屬性的時候,瀏覽器爲了給你最精確的值,須要flush隊列,由於隊列中可能會有影響到這些值的操做。即便你獲取元素的佈局和樣式信息跟最近發生或改變的佈局信息無關,瀏覽器都會強行刷新渲染隊列。

儘可能減小回流和重繪

由於迴流的開銷很大,因此咱們在寫代碼的時候,有不少須要注意的地方:

  • 不要一個一個改變元素的樣式屬性,最好直接改變className,但className是預先定義好的樣式,不是動態的,若是你要動態改變一些樣式,則使用cssText來改變,以下:
// 很差的寫法  
var left = 1;  
var top = 1;  
el.style.left = left + "px";  
el.style.top  = top  + "px";  
 
// 比較好的寫法   
el.className += " className1";  
 
// 比較好的寫法   
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • 讓要操做的元素進行"離線處理",處理完後一塊兒更新,這裏所謂的"離線處理"即讓元素不存在於render tree中

a、使用documentFragment或div等元素進行緩存操做,這個主要用於添加元素的時候,你們應該都用過,就是先把全部要添加到元素添加到1個div(這個div也是新加的),最後才把這個div append到body中。
b、先display:none 隱藏元素,而後對該元素進行全部的操做,最後再顯示該元素。因對display:none的元素進行操做不會引發迴流、重繪。因此只要操做只會有2次迴流。

  • 不要常常訪問會引發瀏覽器flush隊列的屬性,若是你確實要訪問,就先讀取到變量中進行緩存,之後用的時候直接讀取變量就能夠了,見下面代碼:
// 別這樣寫 
for(循環) {  
    elel.style.left = el.offsetLeft + 5 + "px";  
    elel.style.top  = el.offsetTop  + 5 + "px";  
}  
 
// 這樣寫好點  
var left = el.offsetLeft,top  = el.offsetTop,s = el.style;  
for(循環) {  
    left += 10;  
    top  += 10;  
    s.left = left + "px";  
    s.top  = top  + "px";  
}
  • 考慮你的操做會影響到render
    tree中的多少節點以及影響的方式,影響越多,花費確定就越多。好比如今不少人使用jquery的animate方法移動元素來展現一些動畫效果,想一想下面2種移動的方法:
// block1是position:absolute 定位的元素,它移動會影響到它父元素下的全部子元素。  
// 由於在它移動過程當中,全部子元素須要判斷block1的z-index是否在本身的上面,  
// 若是是在本身的上面,則須要重繪,這裏不會引發迴流  
$("#block1").animate({left:50});  
// block2是相對定位的元素,這個影響的元素與block1同樣,可是由於block2非絕對定位  
// 並且改變的是marginLeft屬性,因此這裏每次改變不但會影響重繪,  
// 還會引發父元素及其下元素的迴流  
$("#block2").animate({marginLeft:50});

參考文章:
https://www.cnblogs.com/kevin...
https://blog.csdn.net/allenli...
https://www.css88.com/archive...

相關文章
相關標籤/搜索