DOM 操做成本到底高在哪兒?

從我接觸前端到如今,一直聽到的一句話:操做DOM的成本很高,不要輕易去操做DOM。尤爲是React、vue等MV*框架的出現,數據驅動視圖的模式愈加深刻人心,jQuery時代提供的強大便利地操做DOM的API在前端工程裏用的愈來愈少。刨根問底,這裏說的成本,到底高在哪兒呢?css

什麼是DOM

Document Object Model 文檔對象模型html

什麼是DOM?可能不少人第一反應就是div、p、span等html標籤(至少我是),但要知道,DOM是Model,是Object Model,對象模型,是爲HTML(and XML)提供的API。HTML(Hyper Text Markup Language)是一種標記語言,HTML在DOM的模型標準中被視爲對象,DOM只提供編程接口,卻沒法實際操做HTML裏面的內容。但在瀏覽器端,前端們能夠用腳本語言(JavaScript)經過DOM去操做HTML內容。前端

那麼問題來了,只有JavaScript才能調用DOM這個API嗎?vue

答案是NOnode

Python也能夠訪問DOM。因此DOM不是提供給Javascript的API,也不是Javascript裏的API。web

PS: 實質上還存在CSSOM:CSS Object Model,瀏覽器將CSS代碼解析成樹形的數據結構,與DOM是兩個獨立的數據結構。chrome

瀏覽器渲染過程

討論DOM操做成本,確定要先了解該成本的來源,那麼就離不開瀏覽器渲染。編程

這裏暫只討論瀏覽器拿到HTML以後開始解析、渲染。(怎麼拿到HTML資源的可能後續另開篇總結吧,什麼握握握手啊揮揮揮揮手啊,萬惡的flag...)瀏覽器

  1. 解析HTML,構建DOM樹(這裏遇到外鏈,此時會發起請求)緩存

  2. 解析CSS,生成CSS規則樹

  3. 合併DOM樹和CSS規則,生成render樹

  4. 佈局render樹(Layout/reflow),負責各元素尺寸、位置的計算

  5. 繪製render樹(paint),繪製頁面像素信息

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

1.構建DOM樹
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>


 

 

 

 

 

 

 

 

 

 

 

 

不管是DOM仍是CSSOM,都是要通過 Bytes→characters→tokens→nodes→objectmodel這個過程。

DOM樹構建過程:當前節點的全部子節點都構建好後纔會去構建當前節點的下一個兄弟節點。

2.構建CSSOM樹

上述也提到了CSSOM的構建過程,也是樹的結構,在最終計算各個節點的樣式時,瀏覽器都會先從該節點的廣泛屬性(好比body裏設置的全局樣式)開始,再去應用該節點的具體屬性。還有要注意的是,每一個瀏覽器都有本身默認的樣式表,所以不少時候這棵CSSOM樹只是對這張默認樣式表的部分替換。

3.生成render樹

DOM樹和CSSOM樹合併生成render樹

簡單描述這個過程:

DOM樹從根節點開始遍歷可見節點,這裏之因此強調了「可見」,是由於若是遇到設置了相似 display:none;的不可見節點,在render過程當中是會被跳過的(但 visibility:hidden;opacity:0這種仍舊佔據空間的節點不會被跳過render),保存各個節點的樣式信息及其他節點的從屬關係。

4.Layout 佈局

有了各個節點的樣式信息和屬性,但不知道各個節點的確切位置和大小,因此要經過佈局將樣式信息和屬性轉換爲實際可視窗口的相對大小和位置。

5.Paint 繪製

萬事俱備,最後只要將肯定好位置大小的各節點,經過GPU渲染到屏幕的實際像素。

Tips
  • 在上述渲染過程當中,前3點可能要屢次執行,好比js腳本去操做dom、更改css樣式時,瀏覽器又要從新構建DOM、CSSOM樹,從新render,從新layout、paint;

  • Layout在Paint以前,所以每次Layout從新佈局(reflow 迴流)後都要從新出發Paint渲染,這時又要去消耗GPU;

  • Paint不必定會觸發Layout,好比改個顏色改個背景;(repaint 重繪)

  • 圖片下載完也會從新出發Layout和Paint;

什麼時候觸發reflow和repaint

reflow(迴流): 根據Render Tree佈局(幾何屬性),意味着元素的內容、結構、位置或尺寸發生了變化,須要從新計算樣式和渲染樹;

repaint(重繪): 意味着元素髮生的改變隻影響了節點的一些樣式(背景色,邊框顏色,文字顏色等),只須要應用新樣式繪製這個元素就能夠了;

reflow迴流的成本開銷要高於repaint重繪,一個節點的迴流每每回致使子節點以及同級節點的迴流;

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

引發reflow迴流

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

  1. 頁面第一次渲染(初始化)

  2. DOM樹變化(如:增刪節點)

  3. Render樹變化(如:padding改變)

  4. 瀏覽器窗口resize

  5. 獲取元素的某些屬性:

瀏覽器爲了得到正確的值也會提早觸發迴流,這樣就使得瀏覽器的優化失效了,這些屬性包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、調用了getComputedStyle()或者IE的currentStyle

引發repaint重繪
  1. reflow迴流一定引發repaint重繪,重繪能夠單獨觸發

  2. 背景色、顏色、字體改變(注意:字體大小發生變化時,會觸發迴流)

優化reflow、repaint觸發次數
  • 避免逐個修改節點樣式,儘可能一次性修改

  • 使用DocumentFragment將須要屢次修改的DOM元素緩存,最後一次性append到真實DOM中渲染

  • 能夠將須要屢次修改的DOM元素設置 display:none,操做完再顯示。(由於隱藏元素不在render樹內,所以修改隱藏元素不會觸發迴流重繪)

  • 避免屢次讀取某些屬性(見上)

  • 將複雜的節點元素脫離文檔流,下降回流成本

爲何一再強調將css放在頭部,將js文件放在尾部

DOMContentLoaded 和 load
  • DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片...

  • load 事件觸發時,頁面上全部的DOM,樣式表,腳本,圖片都已加載完成

CSS 資源阻塞渲染

構建Render樹須要DOM和CSSOM,因此HTML和CSS都會阻塞渲染。因此須要讓CSS儘早加載(如:放在頭部),以縮短首次渲染的時間。

JS 資源

阻塞瀏覽器的解析,也就是說發現一個外鏈腳本時,需等待腳本下載完成並執行後纔會繼續解析HTML。

這和以前文章提到的瀏覽器線程有關,瀏覽器中js引擎線程和渲染線程是互斥的,詳見《從setTimeout-setInterval看JS線程》

普通的腳本會阻塞瀏覽器解析,加上defer或async屬性,腳本就變成異步,可等到解析完畢再執行。

  • async異步執行,異步下載完畢後就會執行,不確保執行順序,必定在onload前,但不肯定在DOMContentLoaded事件的先後

  • defer延遲執行,相對於放在body最後(理論上在DOMContentLoaded事件前)

舉個栗子
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="app.js"></script>
  </body>
</html>

 

 

 

 

 

 

 

 

 

 

 

 

  • 瀏覽器拿到HTML後,從上到下順序解析文檔

  • 此時遇到css、js外鏈,則同時發起請求

  • 開始構建DOM樹

  • 這裏要特別注意,因爲有CSS資源,CSSOM還未構建前,會阻塞js(若是有的話)

  • 不管JavaScript是內聯仍是外鏈,只要瀏覽器遇到 script 標記,喚醒 JavaScript解析器,就會進行暫停 blocked 瀏覽器解析HTML,並等到 CSSOM 構建完畢,才執行js腳本

  • 渲染首屏(DOMContentLoaded 觸發,其實不必定是首屏,可能在js腳本執行前DOM樹和CSSOM已經構建完render樹,已經paint)

首屏優化Tips

說了這麼多,其實能夠總結幾點瀏覽器首屏渲染優化的方向:

  • 減小資源請求數量(內聯亦或是延遲動態加載)

  • 使CSS樣式表儘早加載,減小@import的使用,由於須要解析完樣式表中全部import的資源纔會算CSS資源下載完

  • 異步js:阻塞解析器的 JavaScript 會強制瀏覽器等待 CSSOM 並暫停 DOM 的構建,致使首次渲染的時間延遲

  • so on...

知道操做DOM成本多高了嗎?

其實寫了這麼多,感受偏題了,大量的資料參考的是chrome開發者文檔。感受js腳本資源那塊仍是有點亂,包括和DOMContentLoaded的關係,但願你們能多多指點,多多批評,謝謝大佬們。

操做DOM具體的成本,說究竟是形成瀏覽器迴流reflow和重繪reflow,從而消耗GPU資源。

相關文章
相關標籤/搜索