瀏覽器渲染頁面原理,reflow、repaint及其優化

瀏覽器的主要組件包括:

1.      用戶界面 - 包括地址欄、前進/後退按鈕、書籤菜單等。除了瀏覽器主窗口顯示的你請求的頁面外,其餘顯示的各個部分都屬於用戶界面。javascript

2.      瀏覽器引擎 - 在用戶界面和渲染引擎之間傳送指令。css

3.      渲染引擎 - 負責顯示請求的內容。若是請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在屏幕上。html

4.      網絡 - 用於網絡調用,好比 HTTP 請求。其接口與平臺無關,併爲全部平臺提供底層實現。java

5.      用戶界面後端 - 用於繪製基本的窗口小部件,好比組合框和窗口。其公開了與平臺無關的通用接口,而在底層使用操做系統的用戶界面方法。node

6.      JavaScript 解釋器。用於解析和執行 JavaScript 代碼,好比chrome的JavaScript解釋器是V8。web

7.      數據存儲。這是持久層。瀏覽器須要在硬盤上保存各類數據,例如 Cookie。新的 HTML 規範 (HTML5)定義了「網絡數據庫」,這是一個完整(可是輕便)的瀏覽器內數據庫。chrome

 

關鍵路徑渲染(Critical Rendering Path):漸進式數據庫

 

瀏覽器拿到HTML以後的渲染過程:(不一樣內核實現不同但大概是這樣)express

1.      解析HTML,構建DOM tree。segmentfault

2.      解析CSS,構建CSSOM tree。

3.      合併DOM tree和CSSOM tree,生成render tree。

4.      佈局(layout/reflow),計算各元素尺寸、位置。

5.      繪製(paint/repaint),繪製頁面像素信息。

6.      瀏覽器將各層的信息發送給GPU,GPU將各層合成,顯示在屏幕上。

當修改了DOM或CSSOM,上述過程當中的一些步驟就會重複執行。

 

構建OM:要通過Bytes  characters  tokens  nodes  object model這個過程。

 

TIPS:

解析HTML遇到外部CSS當即請求 ----CSS文件合併,減小HTTP請求;

新的CSS style修改CSSOM,會從新渲染頁面 ----CSS文件應放在頭部,縮短首次渲染時間

遇到<img>會發出請求,但不會阻塞,服務器返回圖片文件,因爲圖片佔用了必定面積,影響了後面段落的排布,所以瀏覽器須要回過頭來從新渲染這部分代碼;(最好圖片都設置尺寸,避免從新渲染)

遇到<script> 標籤,會當即執行js代碼,阻塞渲染。(script最好放置頁面最下面)

js修改DOM會從新渲染。 (頁面初始化樣式不要使用js控制) 

 

reflow迴流:

當某個部分發生了變化影響了佈局,須要倒回去從新渲染, 該過程稱爲reflow(迴流)。reflow 幾乎是沒法避免的。如今界面上流行的一些效果,好比樹狀目錄的摺疊、展開(實質上是元素的顯 示與隱藏)等,都將引發瀏覽器的 reflow。鼠標滑過、點擊……只要這些行爲引發了頁面上某些元素的佔位面積、定位方式、邊距等屬性的變化,都會引發它內部、周圍甚至整個頁面的從新渲染。一般咱們沒法預估瀏覽器到底會 reflow 哪一部分的代碼,它們彼此相互影響。

repaint重繪:

若是隻是改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部佈局的屬性,將只會引發瀏覽器 repaint(重繪)。repaint 的速度明顯快於 reflow(在IE下須要換一下說法,reflow 要比 repaint 更緩慢)。

reflow必定引發repaint,而repaint不必定要reflow。reflow的成本比repaint高不少,DOM tree裏每一個結點的reflow極可能觸發其子結點、祖先結點、兄弟結點的reflow。reflow(迴流)是致使DOM腳本執行低效的關鍵因素之一。

 現代瀏覽器會對迴流作優化,它會等到足夠數量的變化發生,再作一次批處理迴流。

 GoogleChromeLabs裏面有一個csstriggers,列出了各個CSS屬性對瀏覽器執行Layout、Paint、Composite的影響。

 

在哪些狀況下會致使reflow發生:

l  改變窗囗大小

l  改變文字大小

l  添加/刪除樣式表

l  內容的改變,如用戶在輸入框中敲字

l  激活僞類,如:hover (IE裏是一個兄弟結點的僞類被激活)

l  操做class屬性

l  腳本操做DOM

l  計算offsetWidth和offsetHeight

l  設置style屬性

 

優化,儘可能避免reflow:

l  儘量限制reflow的影響範圍,修改DOM層級較低的結點。不要經過父級元素影響子元素樣式。最好直接加在子元素上。改變子元素樣式儘量不要影響父元素和兄弟元素的尺寸。

l  不要一條一條的修改DOM的style,最好經過設置class的方式。 避免觸發屢次reflow和repaint。

l  常常reflow的元素,好比動畫,position設爲fixed或absolute,使其脫離文檔流,不影響其它元素的佈局。

l  權衡速度的平滑。好比實現一個動畫,以1個像素爲單位移動這樣最平滑,但reflow就會過於頻繁,CPU很快就會被徹底佔用。若是以3個像素爲單位移動就會好不少。

l  不要用tables佈局。tables中某個元素一旦觸發reflow就會致使table裏全部的其它元素reflow。在適合用table的場合,能夠設置table-layout爲auto或fixed,這樣可讓table一行一行的渲染,這種作法也是爲了限制reflow的影響範圍。

l  避免使用css expression(每次都會從新計算)。

l  減小沒必要要的 DOM 層級(DOM depth)。改變 DOM 樹中的一級會致使全部層級的改變,上至根部,下至被改變節點的子節點。這致使大量時間耗費在執行 reflow 上面。

l  避免沒必要要的複雜的 CSS 選擇器,尤爲是後代選擇器(descendant selectors),由於爲了匹配選擇器將耗費更多的 CPU。

l  儘可能不要頻繁的增長、修改、刪除元素,能夠先把DOM節點抽離到內存中進行復雜的操做而後再display到頁面上。(display:none的節點不會被加入render tree,而visibility:hidden會;display:none會觸發reflow,而visibility:hidden只會觸發repaint,由於layout沒有變化)。

 

讓要進行復雜操做的元素進行「離線處理」,處理完後一塊兒更新:

1.          使用DocumentFragment, DocumentFragment節點不屬於文檔樹,繼承的parentNode屬性老是null。

 

 
//不建議的作法
 
for(var i = 0 ; i < 10000; i ++) {
 
var p = document.createElement("p");
 
var oTxt = document.createTextNode("段落" + i);
 
p.appendChild(oTxt);
 
document.body.appendChild(p);
 
}
 
//將這些元素添加到DocumentFragment中,再將DocumentFragment添加到頁面
 
var oFragment = document.createDocumentFragment();
 
for(var i = 0 ; i < 10000; i ++) {
 
var p = document.createElement("p");
 
var oTxt = document.createTextNode("段落" + i);
 
p.appendChild(oTxt);
 
oFragment.appendChild(p);
 
}
 
document.body.appendChild(oFragment);

  

jQuery的 append等方法內部也是經過createDocumentFragment來實現的,最好在循環外一次性批量添加DOM元素。 

 
//不建議的作法
 
 
 
varbrowserList = ["IE", "Mozilla Firefox", "Safari", "Chrome", "Opera"];
 
 
 
$.each(browserList, function (index,value) {
 
 
 
$('<li>').text(value).appendTo($('ul').eq(0));
 
 
 
})
 
 
 
//好的作法NO1
 
 
 
varbrowserList = ["IE", "Mozilla Firefox", "Safari", "Chrome", "Opera"];
 
 
 
varmyHTML = '';
 
 
 
$.each(browserList, function (index, value) {
 
 
 
myHTML += '<li>' + value + '</li>';
 
 
 
})
 
 
 
$('ul').eq(0).html(myHTML);
 
 
 
//好的做法NO2
 
 
 
var frag= document.createDocumentFragment();
 
 
 
varbrowserList = ["IE", "Mozilla Firefox", "Safari", "Chrome", "Opera"];
 
 
 
$.each(browserList, function (index, value) {
 
 
 
varli = document.createElement("li");
 
 
 
li.textContent = value;
 
 
 
frag.appendChild(li);
 
 
 
})
 
 
 
$('ul').eq(0).append($(frag));

  

2.      使用display:none,先隱藏後顯示,只會引發兩次reflow和repaint。因display:none的元素不在render tree,對其操做不會引發其餘元素的reflow和repaint。

3.      使用cloneNode和replaceChild,引起一次reflow和repaint。

 

阻塞渲染:CSS 與 JavaScript

談論資源的阻塞時,咱們要清楚,現代瀏覽器老是並行加載資源。例如,當 HTML 解析器(HTML Parser)被腳本阻塞時,解析器雖然會中止構建 DOM,但仍會識別該腳本後面的資源,並進行預加載。

同時,因爲下面兩點:

1. 默認狀況下,CSS 被視爲阻塞渲染的資源,這意味着瀏覽器將不會渲染任何已處理的內容,直至 CSSOM 構建完畢。

2. JavaScript 不只能夠讀取和修改 DOM 屬性,還能夠讀取和修改 CSSOM 屬性。

存在阻塞的 CSS 資源時,瀏覽器會延遲 JavaScript 的執行和 DOM 構建。另外:

1. 當瀏覽器遇到一個 script 標記時,DOM 構建將暫停,直至腳本完成執行。

2. JavaScript 能夠查詢和修改 DOM 與 CSSOM。

3. CSSOM 構建時,JavaScript 執行將暫停,直至 CSSOM 就緒。

因此,script 標籤的位置很重要。實際使用時,能夠遵循下面兩個原則:

1. CSS 優先:引入順序上,CSS 資源先於 JavaScript 資源。

2. JavaScript 應儘可能少影響 DOM 的構建。

瀏覽器的發展日益加快(目前的 Chrome 官方穩定版是 61),具體的渲染策略會不斷進化,但瞭解這些原理後,就能想通它進化的邏輯。下面來看看 CSS 與 JavaScript 具體會怎樣阻塞資源。

CSS

<style>p{color:red;}</style>
<link rel="stylesheet" href="index.css">

  

這樣的 link 標籤(不管是否 inline)會被視爲阻塞渲染的資源,瀏覽器會優先處理這些 CSS 資源,直至 CSSOM 構建完畢。

渲染樹(Render-Tree)的關鍵渲染路徑中,要求同時具備 DOM 和 CSSOM,以後纔會構建渲染樹。即,HTML 和 CSS 都是阻塞渲染的資源。HTML 顯然是必需的,由於包括咱們但願顯示的文本在內的內容,都在 DOM 中存放,那麼能夠從CSS 上想辦法。

最容易想到的固然是精簡 CSS 並儘快提供它。除此以外,還能夠用媒體類型(media type)和媒體查詢(media query)來解除對渲染的阻塞。

<link href="index.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">

  

第一個資源會加載並阻塞。
第二個資源設置了媒體類型,會加載但不會阻塞,print 聲明只在打印網頁時使用。
第三個資源提供了媒體查詢,會在符合條件時阻塞渲染。

JavaScript

JavaScript的狀況比 CSS 要更復雜一些。觀察下面的代碼:

<p>Do not go gentle into that good night,</p>
<script>console.log("inline")</script>
<p>Old age should burn and rave at close of day;</p>
<script src="app.js"></script>
<p>Rage, rage against the dying of the light.</p>
 
<p>Do not go gentle into that good night,</p>
<script src="app.js"></script>
<p>Old age should burn and rave at close of day;</p>
<script>console.log("inline")</script>
<p>Rage, rage against the dying of the light.</p>

  

這樣的 script 標籤會阻塞 HTML 解析,不管是否是 inline-script。上面的 P 標籤會從上到下解析,這個過程會被兩段JavaScript 分別打斷一次(加載而且執行的時間段內)。

因此實際工程中,咱們經常將資源放到文檔底部。

改變阻塞模式:defer 與 async

爲何要將 script 加載的 defer 與 async 方式放到後面呢?由於這兩種方式是的出現,全是因爲前面講的那些阻塞條件的存在。換句話說,defer 與 async 方式能夠改變以前的那些阻塞情形。

首先,注意 async 與 defer 屬性對於 inline-script 都是無效的,因此下面這個示例中三個 script 標籤的代碼會從上到下依次執行。

<!-- 按照從上到下的順序輸出 1 2 3 -->
<script async>
  console.log("1");
</script>
<script defer>
  console.log("2");
</script>
<script>
  console.log("3");
</script>

  

 

故,下面兩節討論的內容都是針對設置了 src 屬性的 script 標籤。

defer

<script src="app1.js" defer></script>
<script src="app2.js" defer></script>
<script src="app3.js" defer></script>

  

defer屬性表示延遲執行引入的 JavaScript,即這段 JavaScript 加載時 HTML 並未中止解析,這兩個過程是並行的。整個document 解析完畢且 defer-script 也加載完成以後(這兩件事情的順序無關),會執行全部由 defer-script 加載的JavaScript 代碼,而後觸發 DOMContentLoaded 事件。

defer不會改變 script 中代碼的執行順序,示例代碼會按照 一、二、3 的順序執行。因此,defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標籤解析完成以後。

async

<script src="app.js" async></script>
<script src="ad.js" async></script>
<script src="statistics.js" async></script>

  

async屬性表示異步執行引入的 JavaScript,與 defer 的區別在於,若是已經加載好,就會開始執行——不管此刻是HTML 解析階段仍是 DOMContentLoaded 觸發以後。須要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發以前或以後執行,但必定在 load 觸發以前執行。

從上一段也能推出,多個 async-script 的執行順序是不肯定的。值得注意的是,向 document 動態添加 script 標籤時,async 屬性默認是 true,下一節會繼續這個話題。

document.createElement

使用 document.createElement 建立的 script 默認是異步的,示例以下。

console.log(document.createElement("script").async);// true

  

因此,經過動態添加 script 標籤引入 JavaScript 文件默認是不會阻塞頁面的。若是想同步執行,須要將 async 屬性人爲設置爲 false。

若是使用 document.createElement 建立 link 標籤會怎樣呢?

conststyle=document.createElement("link");
style.rel="stylesheet";
style.href="index.css";
document.head.appendChild(style);// 阻塞?

  

其實這隻能經過試驗肯定,已知的是,Chrome 中已經不會阻塞渲染,Firefox、IE 在之前是阻塞的,如今會怎樣我沒有試驗。

document.write 與 innerHTML

經過 document.write 添加的 link 或 script 標籤都至關於添加在 document 中的標籤,由於它操做的是 document stream(因此對於 loaded 狀態的頁面使用 document.write 會自動調用 document.open,這會覆蓋原有文檔內容)。即正常狀況下, link 會阻塞渲染,script 會同步執行。不過這是不推薦的方式,Chrome 已經會顯示警告,提示將來有可能禁止這樣引入。若是給這種方式引入的 script 添加 async 屬性,Chrome 會檢查是否同源,對於非同源的 async-script 是不容許這麼引入的。

若是使用 innerHTML 引入 script 標籤,其中的 JavaScript 不會執行。固然,能夠經過 eval() 來手工處理,不過不推薦。若是引入 link 標籤,我試驗過在 Chrome 中是能夠起做用的。另外,outerHTML、insertAdjacentHTML() 應該也是相同的行爲,我並無試驗。這三者應該用於文本的操做,即只使用它們添加 text 或普通 HTML Element。

  

參考:

http://www.cnblogs.com/Peng2014/p/4687218.html

http://www.javashuo.com/article/p-kqratyvy-cr.html

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/

https://zhuanlan.zhihu.com/p/29418126 

相關文章
相關標籤/搜索