從我接觸前端到現在,一直聽到的一句話:操作DOM的成本很高,不要輕易去操作DOM。尤其是React、vue等MV*框架的出現,資料驅動檢視的模式越發深入人心,jQuery時代提供的強大便利地操作DOM的API在前端工程裡用的越來越少。刨根問底,這裡說的成本,到底高在哪兒呢?
什麼是DOM
Document Object Model 文件物件模型
什麼是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嗎?
答案是NO。
Python也可以訪問DOM。所以DOM不是提供給Javascript的API,也不是Javascript裡的API。
PS: 實質上還存在CSSOM:CSS Object Model,瀏覽器將CSS程式碼解析成樹形的資料結構,與DOM是兩個獨立的資料結構。
瀏覽器渲染過程
討論DOM操作成本,肯定要先了解該成本的來源,那麼就離不開瀏覽器渲染。
這裡暫只討論瀏覽器拿到HTML之後開始解析、渲染。(怎麼拿到HTML資源的可能後續另開篇總結吧,什麼握握握手啊揮揮揮揮手啊,萬惡的flag...)
-
解析HTML,構建DOM樹(這裡遇到外鏈,此時會發起請求)
-
解析CSS,生成CSS規則樹
-
合併DOM樹和CSS規則,生成render樹
-
佈局render樹(Layout/reflow),負責各元素尺寸、位置的計算
-
繪製render樹(paint),繪製頁面畫素資訊
-
瀏覽器會將各層的資訊傳送給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 → object model
這個過程。
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迴流
現代瀏覽器會對迴流做優化,它會等到足夠數量的變化發生,再做一次批處理迴流。
- 頁面第一次渲染(初始化)
- DOM樹變化(如:增刪節點)
- Render樹變化(如:padding改變)
- 瀏覽器視窗resize
- 獲取元素的某些屬性: 瀏覽器為了獲得正確的值也會提前觸發迴流,這樣就使得瀏覽器的優化失效了,這些屬性包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、呼叫了getComputedStyle()或者IE的currentStyle
引起repaint重繪
- reflow迴流必定引起repaint重繪,重繪可以單獨觸發
- 背景色、顏色、字型改變(注意:字型大小發生變化時,會觸發迴流)
優化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資源。
參考文獻:
developers.google.com/web/fundame…
已同步至個人部落格-軟硬皆施
Github 歡迎star :)