常用的 DOM 優(yōu)化
隨著用戶體驗(yàn)的日益重視,前端性能對用戶體驗(yàn)的影響備受關(guān)注,但由于引起性能問題的原因相對復(fù)雜,我們很難從某一方面或某幾個(gè)方面來全面解決它,接下來用一系列文章來深層次探討與梳理有關(guān) Javascript 性能的方方面面,以填補(bǔ)并夯實(shí)大家的知識結(jié)構(gòu)。
接下來我們來聊一聊關(guān)于 DOM 操作相關(guān)的性能優(yōu)化。前端工程師,一直說的一句話:操作 DOM 的成本很高,不要輕易去操作 DOM。尤其是 React、vue 等 MV*框架的出現(xiàn),數(shù)據(jù)驅(qū)動(dòng)視圖的模式越發(fā)深入人心,jQuery 時(shí)代提供的強(qiáng)大便利地操作 DOM 的 API 在前端工程里用的越來越少。刨根問底,這里說的成本,到底高在哪兒呢?
DOM 操作成本到底高在哪兒?
什么是 DOM?可能很多人第一反應(yīng)就是 div、p、span 等 html 標(biāo)簽(至少我是),但要知道,DOM 是 Model,是 Object Model,對象模型,是為 HTML(and XML)提供的 API。HTML(Hyper Text Markup Language)是一種標(biāo)記語言,HTML 在 DOM 的模型標(biāo)準(zhǔn)中被視為對象,DOM 只提供編程接口,卻無法實(shí)際操作 HTML 里面的內(nèi)容。但在瀏覽器端,前端們可以用腳本語言(JavaScript)通過 DOM 去操作 HTML 內(nèi)容。
實(shí)質(zhì)上還存在 CSSOM:CSS Object Model,瀏覽器將 CSS 代碼解析成樹形的數(shù)據(jù)結(jié)構(gòu),與DOM 是兩個(gè)獨(dú)立的數(shù)據(jù)結(jié)構(gòu)。
接下來說一說瀏覽器渲染。過程。
討論 DOM 操作成本,肯定要先了解該成本的來源,那么就離不開瀏覽器渲染。
解析 HTML,構(gòu)建 DOM 樹(這里遇到外鏈,此時(shí)會(huì)發(fā)起請求)
解析 CSS,生成 CSS 規(guī)則樹
合并 DOM 樹和 CSS 規(guī)則,生成 render 樹
布局 render 樹(Layout/reflow),負(fù)責(zé)各元素尺寸、位置的計(jì)算
繪制 render 樹(paint),繪制頁面像素信息
瀏覽器會(huì)將各層的信息發(fā)送給 GPU,GPU 將各層合成(composite),顯示在屏幕上
1.構(gòu)建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,都是要經(jīng)過 Bytes → characters → tokens → nodes →object model 這個(gè)過程。
DOM 樹構(gòu)建過程:當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)都構(gòu)建好后才會(huì)去構(gòu)建當(dāng)前節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn)。屬于深度優(yōu)先遍歷過程。
2.構(gòu)建CSSOM 樹
上述也提到了 CSSOM 的構(gòu)建過程,也是樹的結(jié)構(gòu),在最終計(jì)算各個(gè)節(jié)點(diǎn)的樣式時(shí),瀏覽器都會(huì)先從該節(jié)點(diǎn)的普遍屬性(比如 body 里設(shè)置的全局樣式)開始,再去應(yīng)用該節(jié)點(diǎn)的具體屬性。還有要注意的是,每個(gè)瀏覽器都有自己默認(rèn)的樣式表,因此很多時(shí)候這棵 CSSOM 樹只是對這張默認(rèn)樣式表的部分替換。
3.生成render 樹
簡單描述這個(gè)過程:
DOM 樹從根節(jié)點(diǎn)開始遍歷可見節(jié)點(diǎn),這里之所以強(qiáng)調(diào)了“可見”,是因?yàn)槿绻龅皆O(shè)置了類似 display: none;的不可見節(jié)點(diǎn),在 render 過程中是會(huì)被跳過的(但 visibility: hidden; opacity: 0 這種仍舊占據(jù)空間的節(jié)點(diǎn)不會(huì)被跳過 render),保存各個(gè)節(jié)點(diǎn)的樣式信息及其余節(jié)點(diǎn)的從屬關(guān)系。
4. Layout 布局
有了各個(gè)節(jié)點(diǎn)的樣式信息和屬性,但不知道各個(gè)節(jié)點(diǎn)的確切位置和大小,所以要通過布局將樣式信息和屬性轉(zhuǎn)換為實(shí)際可視窗口的相對大小和位置。
5.Paint 繪制
萬事俱備,最后只要將確定好位置大小的各節(jié)點(diǎn),通過 GPU 渲染到屏幕的實(shí)際像素。
Tips
在上述渲染過程中,前 3 點(diǎn)可能要多次執(zhí)行,比如 js 腳本去操作 dom、更改 css 樣式時(shí),瀏覽器又要重新構(gòu)建 DOM、CSSOM 樹,重新 render,重新 layout、paint;
Layout 在 Paint 之前,因此每次 Layout 重新布局(reflow 回流)后都要重新出發(fā) Paint 渲染,這時(shí)又要去消耗 GPU;
Paint 不一定會(huì)觸發(fā) Layout,比如改個(gè)顏色改個(gè)背景;(repaint 重繪)
圖片下載完也會(huì)重新觸發(fā) Layout 和 Paint;
何時(shí)觸發(fā) reflow 和 repaint
reflow(回流):根據(jù) Render Tree 布局(幾何屬性),意味著元素的內(nèi)容、結(jié)構(gòu)、位置或尺寸發(fā)生了變化,需要重新計(jì)算樣式和渲染樹;
repaint(重繪): 意味著元素發(fā)生的改變只影響了節(jié)點(diǎn)的一些樣式(背景色,邊框顏色, 文字顏色等),只需要應(yīng)用新樣式繪制這個(gè)元素就可以了;
reflow 回流的成本開銷要高于 repaint 重繪,一個(gè)節(jié)點(diǎn)的回流往往會(huì)導(dǎo)致子節(jié)點(diǎn)以及同級節(jié)點(diǎn)的回流;
引起 reflow 回流
現(xiàn)代瀏覽器會(huì)對回流做優(yōu)化,它會(huì)等到足夠數(shù)量的變化發(fā)生,再做一次批處理回流。
頁面第一次渲染(初始化)
DOM 樹變化(如:增刪節(jié)點(diǎn))
Render 樹變化(如:padding 改變)
瀏覽器窗口 resize
獲取元素的某些屬性:瀏覽器為了獲得正確的值也會(huì)提前觸發(fā)回流,這樣就使得瀏覽器的優(yōu)化失效了,這些屬性包括 offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、調(diào)用了 getComputedStyle()或者 IE 的currentStyle
引起 repaint 重繪
reflow 回流必定引起 repaint 重繪,重繪可以單獨(dú)觸發(fā)
背景色、顏色、字體改變(注意:字體大小發(fā)生變化時(shí),會(huì)觸發(fā)回流)
優(yōu)化 reflow、repaint 觸發(fā)次數(shù)
避免逐個(gè)修改節(jié)點(diǎn)樣式,盡量一次性修改
使用 DocumentFragment 將需要多次修改的 DOM 元素緩存,最后一次性 append 到真實(shí) DOM 中渲染
可以將需要多次修改的 DOM 元素設(shè)置 display: none,操作完再顯示。(因?yàn)殡[藏元素不在 render 樹內(nèi),因此修改隱藏元素不會(huì)觸發(fā)回流重繪)
避免多次讀取某些屬性(見上)
將復(fù)雜的節(jié)點(diǎn)元素脫離文檔流,降低回流成本
操作 DOM 具體的成本,說到底是造成瀏覽器回流 reflow 和重繪 reflow,從而消耗 GPU 資源。
既然 DOM 操作是很耗性能的,我們該怎么做盡量的減少性能的損耗呢?
DOM 優(yōu)化常用方法
優(yōu)化節(jié)點(diǎn)修改
使用 cloneNode 在外部更新節(jié)點(diǎn)然后再通過 replace 與原始節(jié)點(diǎn)互換。
var orig = document.getElementById('container');var clone = orig.cloneNode(true);var list = ['foo', 'bar', 'baz'];var content;for (var i = 0; i < list.length; i++) {content = document.createTextNode(list[i]);clone.appendChild(content);}orig.parentNode.replaceChild(clone, orig)
優(yōu)化節(jié)點(diǎn)添加
多個(gè)節(jié)點(diǎn)插入操作,即使在外面設(shè)置節(jié)點(diǎn)的元素和風(fēng)格再插入,由于多個(gè)節(jié)點(diǎn)還是會(huì)引發(fā)多次 reflow。
優(yōu)化的方法是創(chuàng)建 DocumentFragment,在其中插入節(jié)點(diǎn)后再添加到頁面。
如 JQuery 中所有的添加節(jié)點(diǎn)的操作如 append,都是最終調(diào)用DocumentFragment 來實(shí)現(xiàn)的。
createSafeFragment(document) {var list = nodeNames.split( "|" ),safeFrag = document.createDocumentFragment();if (safeFrag.createElement) {while (list.length) {safeFrag.createElement(list.pop(););};};return safeFrag;};
優(yōu)化 CSS 樣式轉(zhuǎn)換。
如果需要?jiǎng)討B(tài)更改 CSS 樣式,盡量采用觸發(fā) reflow 次數(shù)較少的方式。
如以下代碼逐條更改元素的幾何屬性,理論上會(huì)觸發(fā)多次 reflow。
element.style.fontWeight = 'bold' ;element.style.marginLeft= '30px' ;element.style.marginRight = '30px' ;
可以通過直接設(shè)置元素的 className 直接設(shè)置,只會(huì)觸發(fā)一次 reflow。
element.className = 'selectedAnchor' ;減少 DOM 元素?cái)?shù)量
在 console 中執(zhí)行命令查看 DOM 元素?cái)?shù)量。document.getElementsByTagName( '*' ).length
正常頁面的 DOM 元素?cái)?shù)量一般不應(yīng)該超過 1000。
DOM 元素過多會(huì)使 DOM 元素查詢效率,樣式表匹配效率降低,是頁面性能最主要的瓶頸之一。
DOM 操作優(yōu)化
DOM 操作性能問題主要有以下原因。
DOM 元素過多導(dǎo)致元素定位緩慢。
大量的 DOM 接口調(diào)用。
JAVASCRIPT 和 DOM 之間的交互需要通過函數(shù) API 接口來完成,造成延時(shí),尤其是在循環(huán)語句中。
DOM 操作觸發(fā)頻繁的 reflow(layout)和 repaint。
layout 發(fā)生在 repaint 之前,所以 layout 相對來說會(huì)造成更多性能損耗。
reflow(layout)就是計(jì)算頁面元素的幾何信息。
repaint 就是繪制頁面元素。
對 DOM 進(jìn)行操作會(huì)導(dǎo)致瀏覽器執(zhí)行回流 reflow。
解決方案。
純 JAVASCRIPT 執(zhí)行時(shí)間是很短的。
最小化 DOM 訪問次數(shù),盡可能在 js 端執(zhí)行。
如果需要多次訪問某個(gè) DOM 節(jié)點(diǎn),請使用局部變量存儲(chǔ)對它的引用。
謹(jǐn)慎處理 HTML 集合(HTML 集合實(shí)時(shí)聯(lián)系底層文檔),把集合的長度緩存到一個(gè)變量中,并在迭代中使用它,如果需要經(jīng)常操作集合,建議把它拷貝到一個(gè)數(shù)組中。
如果可能的話,使用速度更快的 API,比如 querySelectorAll 和firstElementChild。
要留意重繪和重排。
批量修改樣式時(shí),離線操作 DOM 樹。
使用緩存,并減少訪問布局的次數(shù)。
動(dòng)畫中使用絕對定位,使用拖放代理。
使用事件委托來減少事件處理器的數(shù)量。
優(yōu)化 DOM 交互 >在 JAVASCRIPT 中,DOM 操作和交互要消耗大量時(shí)間,因?yàn)樗鼈兺枰匦落秩菊麄€(gè)頁面或者某一個(gè)部分。
最小化現(xiàn)場更新。
當(dāng)需要訪問的 DOM 部分已經(jīng)已經(jīng)被渲染為頁面中的一部分,那么 DOM操作和交互的過程就是再進(jìn)行一次現(xiàn)場更新。
現(xiàn)場更新是需要針對現(xiàn)場(相關(guān)顯示頁面的部分結(jié)構(gòu))立即進(jìn)行更新,每一個(gè)更改(不管是插入單個(gè)字符還是移除整個(gè)片段),都有一個(gè)性能損耗。
現(xiàn)場更新進(jìn)行的越多,代碼完成執(zhí)行所花的時(shí)間也越長。
多使用 innerHTML。
有兩種在頁面上創(chuàng)建 DOM 節(jié)點(diǎn)的方法:
使用諸如 createElement()和 appendChild()之類的 DOM 方法。
使用 innerHTML。
當(dāng)使用 innerHTML 設(shè)置為某個(gè)值時(shí),后臺(tái)會(huì)創(chuàng)建一個(gè)HTML 解釋器,然后使用內(nèi)部的 DOM 調(diào)用來創(chuàng)建 DOM 結(jié)構(gòu),而非基于 JAVASCRIPT 的 DOM 調(diào)用。由于內(nèi)部方法是編譯好的而非解釋執(zhí)行,故執(zhí)行的更快。對于小的DOM 更改,兩者效率差不多,但對于大的 DOM 更改, innerHTML 要比標(biāo)準(zhǔn)的 DOM 方法創(chuàng)建同樣的 DOM 結(jié)構(gòu)快得多。
回流 reflow。
發(fā)生場景。
改變窗體大小。
更改字體。
添加移除 stylesheet 塊。
內(nèi)容改變哪怕是輸入框輸入文字。
CSS 虛類被觸發(fā)如 :hover。
更改元素的 className。
當(dāng)對 DOM 節(jié)點(diǎn)執(zhí)行新增或者刪除操作或內(nèi)容更改時(shí)。
動(dòng)態(tài)設(shè)置一個(gè) style 樣式時(shí)(比如element.style.width="10px")。
當(dāng)獲取一個(gè)必須經(jīng)過計(jì)算的尺寸值時(shí),比如訪問 offsetWidth、clientHeight 或者其他需要經(jīng)過計(jì)算的 CSS 值。
解決問題的關(guān)鍵,就是限制通過 DOM 操作所引發(fā)回流的次數(shù)。
在對當(dāng)前 DOM 進(jìn)行操作之前,盡可能多的做一些準(zhǔn)備工作,保證 N 次創(chuàng)建,1 次寫入。
在對 DOM 操作之前,把要操作的元素,先從當(dāng)前 DOM 結(jié)構(gòu)中刪除:
通過 removeChild()或者 replaceChild()實(shí)現(xiàn)真正意義上的刪除。
設(shè)置該元素的 display 樣式為“none”。
每次修改元素的 style 屬性都會(huì)觸發(fā)回流操作。
element.style.backgroundColor = "blue";
使用更改 className 的方式替換 style.xxx=xxx 的方式。
使用 style.cssText = '';一次寫入樣式。
避免設(shè)置過多的行內(nèi)樣式。
添加的結(jié)構(gòu)外元素盡量設(shè)置它們的位置為 fixed 或absolute。
避免使用表格來布局。
避免在 CSS 中使用 JavaScript expressions(IE only)。
將獲取的 DOM 數(shù)據(jù)緩存起來。這種方法,對獲取那些會(huì)觸發(fā)回流操作的屬性(比如 offsetWidth 等)尤為重要。
當(dāng)對 HTMLCollection 對象進(jìn)行操作時(shí),應(yīng)該將訪問的次數(shù)盡可能的降至最低,最簡單的,你可以將 length 屬性緩存在一個(gè)本地變量中,這樣就能大幅度的提高循環(huán)的效率。
最后聽一首悅耳的歌放松放松,回憶學(xué)到的東西。
點(diǎn)擊下面
播放音樂
長按二維碼關(guān)注,一起努力。
助力尋人啟事
微信公眾號回復(fù)?加群?一起學(xué)習(xí)。
