引用文章署拟,如有冒犯,私我立即刪除
DOM 操作成本到底高在哪兒歌豺?
從我接觸前端到現(xiàn)在推穷,一直聽到的一句話:操作DOM的成本很高,不要輕易去操作DOM类咧。尤其是React缨恒、vue等MV*框架的出現(xiàn),數(shù)據(jù)驅動視圖的模式越發(fā)深入人心轮听,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代碼解析成樹形的數(shù)據(jù)結構,與DOM是兩個獨立的數(shù)據(jù)結構魂莫。
瀏覽器渲染過程
討論DOM操作成本,肯定要先了解該成本的來源爹耗,那么就離不開瀏覽器渲染耙考。
這里暫只討論瀏覽器拿到HTML之后開始解析、渲染潭兽。(怎么拿到HTML資源的可能后續(xù)另開篇總結吧倦始,什么握握握手啊揮揮揮揮手啊,萬惡的flag...)
解析HTML山卦,構建DOM樹(這里遇到外鏈鞋邑,此時會發(fā)起請求)
解析CSS,生成CSS規(guī)則樹
合并DOM樹和CSS規(guī)則账蓉,生成render樹
布局render樹(Layout/reflow)枚碗,負責各元素尺寸、位置的計算
繪制render樹(paint)铸本,繪制頁面像素信息
瀏覽器會將各層的信息發(fā)送給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樹構建過程:當前節(jié)點的所有子節(jié)點都構建好后才會去構建當前節(jié)點的下一個兄弟節(jié)點陌宿。
2.構建CSSOM樹
上述也提到了CSSOM的構建過程,也是樹的結構波丰,在最終計算各個節(jié)點的樣式時壳坪,瀏覽器都會先從該節(jié)點的普遍屬性(比如body里設置的全局樣式)開始,再去應用該節(jié)點的具體屬性掰烟。還有要注意的是爽蝴,每個瀏覽器都有自己默認的樣式表,因此很多時候這棵CSSOM樹只是對這張默認樣式表的部分替換媚赖。
3.生成render樹
DOM樹和CSSOM樹合并生成render樹
簡單描述這個過程:
DOM樹從根節(jié)點開始遍歷可見節(jié)點霜瘪,這里之所以強調了“可見”,是因為如果遇到設置了類似 display:none;的不可見節(jié)點惧磺,在render過程中是會被跳過的(但 visibility:hidden;opacity:0這種仍舊占據(jù)空間的節(jié)點不會被跳過render)颖对,保存各個節(jié)點的樣式信息及其余節(jié)點的從屬關系。
4.Layout 布局
有了各個節(jié)點的樣式信息和屬性磨隘,但不知道各個節(jié)點的確切位置和大小缤底,所以要通過布局將樣式信息和屬性轉換為實際可視窗口的相對大小和位置。
5.Paint 繪制
萬事俱備番捂,最后只要將確定好位置大小的各節(jié)點个唧,通過GPU渲染到屏幕的實際像素。
Tips
在上述渲染過程中设预,前3點可能要多次執(zhí)行徙歼,比如js腳本去操作dom、更改css樣式時鳖枕,瀏覽器又要重新構建DOM魄梯、CSSOM樹,重新render宾符,重新layout酿秸、paint;
Layout在Paint之前魏烫,因此每次Layout重新布局(reflow 回流)后都要重新出發(fā)Paint渲染辣苏,這時又要去消耗GPU;
Paint不一定會觸發(fā)Layout哄褒,比如改個顏色改個背景稀蟋;(repaint 重繪)
圖片下載完也會重新出發(fā)Layout和Paint;
何時觸發(fā)reflow和repaint
reflow(回流): 根據(jù)Render Tree布局(幾何屬性)呐赡,意味著元素的內容糊治、結構、位置或尺寸發(fā)生了變化罚舱,需要重新計算樣式和渲染樹井辜;
repaint(重繪): 意味著元素發(fā)生的改變只影響了節(jié)點的一些樣式(背景色绎谦,邊框顏色,文字顏色等)粥脚,只需要應用新樣式繪制這個元素就可以了窃肠;
reflow回流的成本開銷要高于repaint重繪,一個節(jié)點的回流往往回導致子節(jié)點以及同級節(jié)點的回流刷允;
GoogleChromeLabs 里面有一個csstriggers冤留,列出了各個CSS屬性對瀏覽器執(zhí)行Layout、Paint树灶、Composite的影響纤怒。
引起reflow回流
現(xiàn)代瀏覽器會對回流做優(yōu)化,它會等到足夠數(shù)量的變化發(fā)生天通,再做一次批處理回流泊窘。
頁面第一次渲染(初始化)
DOM樹變化(如:增刪節(jié)點)
Render樹變化(如:padding改變)
瀏覽器窗口resize
獲取元素的某些屬性:
瀏覽器為了獲得正確的值也會提前觸發(fā)回流,這樣就使得瀏覽器的優(yōu)化失效了像寒,這些屬性包括offsetLeft、offsetTop诺祸、offsetWidth携悯、offsetHeight、 scrollTop/Left/Width/Height筷笨、clientTop/Left/Width/Height憔鬼、調用了getComputedStyle()或者IE的currentStyle
引起repaint重繪
reflow回流必定引起repaint重繪,重繪可以單獨觸發(fā)
背景色胃夏、顏色轴或、字體改變(注意:字體大小發(fā)生變化時,會觸發(fā)回流)
優(yōu)化reflow构订、repaint觸發(fā)次數(shù)
避免逐個修改節(jié)點樣式侮叮,盡量一次性修改
使用DocumentFragment將需要多次修改的DOM元素緩存避矢,最后一次性append到真實DOM中渲染
可以將需要多次修改的DOM元素設置?display:none悼瘾,操作完再顯示。(因為隱藏元素不在render樹內审胸,因此修改隱藏元素不會觸發(fā)回流重繪)
避免多次讀取某些屬性(見上)
將復雜的節(jié)點元素脫離文檔流亥宿,降低回流成本
為什么一再強調將css放在頭部,將js文件放在尾部
DOMContentLoaded 和 load
DOMContentLoaded 事件觸發(fā)時砂沛,僅當DOM加載完成烫扼,不包括樣式表,圖片...
load 事件觸發(fā)時碍庵,頁面上所有的DOM映企,樣式表悟狱,腳本,圖片都已加載完成
CSS 資源阻塞渲染
構建Render樹需要DOM和CSSOM堰氓,所以HTML和CSS都會阻塞渲染挤渐。所以需要讓CSS盡早加載(如:放在頭部),以縮短首次渲染的時間双絮。
JS 資源
阻塞瀏覽器的解析浴麻,也就是說發(fā)現(xiàn)一個外鏈腳本時,需等待腳本下載完成并執(zhí)行后才會繼續(xù)解析HTML囤攀。
這和之前文章提到的瀏覽器線程有關软免,瀏覽器中js引擎線程和渲染線程是互斥的,詳見《從setTimeout-setInterval看JS線程》
普通的腳本會阻塞瀏覽器解析焚挠,加上defer或async屬性膏萧,腳本就變成異步,可等到解析完畢再執(zhí)行宣蔚。
async異步執(zhí)行向抢,異步下載完畢后就會執(zhí)行,不確保執(zhí)行順序胚委,一定在onload前挟鸠,但不確定在DOMContentLoaded事件的前后
defer延遲執(zhí)行,相對于放在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外鏈,則同時發(fā)起請求
開始構建DOM樹
這里要特別注意硅急,由于有CSS資源覆享,CSSOM還未構建前,會阻塞js(如果有的話)
無論JavaScript是內聯(lián)還是外鏈营袜,只要瀏覽器遇到?script?標記撒顿,喚醒?JavaScript解析器,就會進行暫停?blocked?瀏覽器解析HTML荚板,并等到?CSSOM?構建完畢凤壁,才執(zhí)行js腳本
渲染首屏(DOMContentLoaded 觸發(fā),其實不一定是首屏跪另,可能在js腳本執(zhí)行前DOM樹和CSSOM已經構建完render樹拧抖,已經paint)
首屏優(yōu)化Tips
說了這么多,其實可以總結幾點瀏覽器首屏渲染優(yōu)化的方向:
減少資源請求數(shù)量(內聯(lián)亦或是延遲動態(tài)加載)
使CSS樣式表盡早加載免绿,減少@import的使用唧席,因為需要解析完樣式表中所有import的資源才會算CSS資源下載完
異步js:阻塞解析器的 JavaScript 會強制瀏覽器等待 CSSOM 并暫停 DOM 的構建,導致首次渲染的時間延遲
so on...
知道操作DOM成本多高了嗎?
其實寫了這么多,感覺偏題了淌哟,大量的資料參考的是chrome開發(fā)者文檔迹卢。感覺js腳本資源那塊還是有點亂,包括和DOMContentLoaded的關系徒仓,希望大家能多多指點婶希,多多批評,謝謝大佬們蓬衡。
操作DOM具體的成本喻杈,說到底是造成瀏覽器回流reflow和重繪reflow,從而消耗GPU資源狰晚。
參考文獻
https://developers.google.com/web/fundamentals/performance/critical-rendering-path/