這幾天分享一下我看《高性能 JavaScript》的學(xué)習(xí)筆記,希望能對(duì)大家有所幫助。
如果說(shuō)前面兩章日常工作中不太關(guān)注,那么 DOM 優(yōu)化確實(shí)我們必知必會(huì)的知識(shí)點(diǎn)了。
天生就慢
首先,DOM 天生就是非常慢的。為什么呢?因?yàn)?DOM 和 JavaScript 運(yùn)行環(huán)境是兩個(gè)環(huán)境,所以兩者通信只能通過(guò)接口連接。就像是 DOM 和 JavaScript 運(yùn)行時(shí)是兩個(gè)島嶼,中間只能通過(guò)一艘小船來(lái)往。
所以,DOM 優(yōu)化的核心就是盡量將更多的處理停留在 JavaScript 運(yùn)行時(shí)這座小島上,減少使用小船來(lái)往的次數(shù)?;蛘哒f(shuō)盡量在 JavaScript 端處理邏輯,只在必要時(shí)訪問(wèn) DOM。
DOM 的修改
- 由于 API 交互特性,訪問(wèn) DOM 的次數(shù)越多,代碼運(yùn)行的速度必然越慢。
- 修改 DOM 元素有兩種方式:element.innerHTML 和 document.createElement() 。兩者的性能在新版本瀏覽器上差不多,而在老版本瀏覽器中 innerHTML 會(huì)更好。(個(gè)人覺(jué)得 innerHTML 寫(xiě)法也更加清晰)
- 另外一種修改 DOM 的方式是通過(guò) element.cloneNodes() 來(lái)克隆元素節(jié)點(diǎn),修改克隆的元素然后替換 DOM 中的元素。
HTML 集合
我們使用一些 API 能夠獲取 HTML 集合,HTML 集合是一個(gè)帶有 length 屬性酷似數(shù)組的對(duì)象。它有一個(gè)很重要的特性就是 HTML 集合會(huì)隨著 DOM 元素的變化而變化。
- HTML 集合的 length 是會(huì)隨著 DOM 元素?cái)?shù)量變化的,所以不要直接使用 length 屬性。
- 遍歷數(shù)組要比遍歷 HTML 集合的速度快。
- 如果要使用 HTML 集合的 length,可以先將 length 保存為局部變量。
- 多使用局部變量保存和引用 HTML 集合的信息,減少訪問(wèn) DOM 的次數(shù)。
以下 DOM API 能夠獲取 HTML 集合。
- document.getElementByName()
- document.getElementByTagName()
- document.getElementByClass()
- element.childNodes 是通過(guò)某個(gè)元素獲取他的子元素的,獲取的也是 HTML 集合。
更快的 API
對(duì)于獲取元素節(jié)點(diǎn)的 DOM 方法,有只獲取元素節(jié)點(diǎn)和獲取所有節(jié)點(diǎn)兩類(lèi)。性能上顯然前者的性能會(huì)更高。
| 只獲取元素節(jié)點(diǎn) | 獲取所有節(jié)點(diǎn) |
|---|---|
| children | childNodes |
| childElementCount | childNodes.length |
| firstElementChild | firstChild |
| lastElementChild | lastChild |
| nextElementSibling | nextSibling |
| previousElementSibling | previousSibling |
在遍歷 DOM 節(jié)點(diǎn)上,很多 DOM 方法可以做到。
- document.getElementByName()
- document.getElementByTagName()
- document.getElementByClass()
- document.querySelectorAll()
- ……
其中 document.querySelectorAll() 使用了 CSS 選擇器來(lái)獲取 NodeList 數(shù)組對(duì)象。這種寫(xiě)法不僅方便,而且性能上也優(yōu)于其他方法。
所以,如果瀏覽器支持,盡量使用上述的這些方法,因?yàn)檫@些 API 更快!
DOM 繪制過(guò)程
對(duì)于 DOM 性能而言,重繪和重排是最重要的知識(shí)點(diǎn)了。下面先復(fù)習(xí)一下瀏覽器工作原理:
- 瀏覽器解析 HTML 獲取 DOM 樹(shù)。
- 瀏覽器解析 CSS 獲取 CSSOM 規(guī)則樹(shù)。
- 將 CSS 規(guī)則樹(shù)應(yīng)用到 DOM 樹(shù)上構(gòu)成渲染樹(shù)。
- 使用渲染樹(shù)上的樣式計(jì)算尺寸和位置,解析排版。
- 更具渲染樹(shù)屬性生成位圖,即繪制。
- 最后使用瀏覽器 API 呈現(xiàn)這些有排版的位圖。
- 如果頁(yè)面元素尺寸有變化,進(jìn)行重新排版和繪制。
- 如果頁(yè)面無(wú)尺寸變化,而是像背景顏色這樣的變化,則會(huì)進(jìn)行重新繪制。
重排何時(shí)發(fā)生?
- 添加或刪除可見(jiàn)的 DOM 元素
- 元素位置改變
- 元素尺寸改變(包括:margin、padding、border-width、width、height 等屬性)
- 內(nèi)容改變,如:文本改變、圖片尺寸改變。
- 頁(yè)面渲染器初始化
- 瀏覽器創(chuàng)建尺寸改變
立即重排
其實(shí),在瀏覽器中重排是有優(yōu)化機(jī)制的。瀏覽器會(huì)隊(duì)列化修改并批量執(zhí)行重排行為。但是使用了一些 API 后會(huì)立即進(jìn)行重排行為。主要是一些查詢當(dāng)前布局信息的 API 方法:
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop, scrollLeft, scrollWidth, scrollHeight
- clientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle() (IE 中的當(dāng)前樣式)
優(yōu)化方案是不在布局信息改變時(shí)查詢布局信息。
最小化重繪和重排
最小化重繪和重排的方式是盡量減少修改 DOM 的次數(shù)。
// bad
var el = document.getElementById('mydiv')
el.style.borderLeft = '1px'
el.syle.borderRight = '1px'
el.style.padding = '5px'
// good
el.style.cssText = 'border-left: 1px; border-right: 1px; padding: 5px;'
// good
el.className = 'active'
批量修改 DOM 方案很好的減少了修改 DOM 的次數(shù)。大致方案如下:
- 將需要修改的元素節(jié)點(diǎn)脫離文檔流。
- 修改脫離文檔流的元素節(jié)點(diǎn)。
- 將元素帶入文檔流。
具體的方案有三種:
- 將需要修改的 DOM 隱藏(display: none)后對(duì)節(jié)點(diǎn)進(jìn)行修改,最后再將隱藏的節(jié)點(diǎn)顯示出來(lái)。
var ul = document.getElementById('list')
ul.style.display = 'none'
appendDataToElement(ul, data)
ul.style.display = 'block'
- 使用 document.createDocumentFragment() 方法創(chuàng)建文檔片段,在文檔片段中定義修改的 DOM,最后將文檔片段應(yīng)用到 DOM 中。
var fragment = document.createDocumentFragment()
appendDataToElement(fragment, data)
document.getElementById('list').appendChild(fragment)
- 使用 document.cloneNode() 克隆元素節(jié)點(diǎn),修改后用克隆節(jié)點(diǎn)替換原有節(jié)點(diǎn)。
var old = document.getElementById('list')
var clone = old.cloneNode(true)
appendDataToElement(clone, data)
old.parentNode.replaceChild(clone, old)
在基于現(xiàn)有元素尺寸修改尺寸時(shí),最好使用局部變量緩存布局信息,這樣可以減少訪問(wèn) DOM 的次數(shù)。
事件委托
事件監(jiān)聽(tīng)綁定也有一定的性能開(kāi)銷(xiāo),可以使用事件委托方法來(lái)減少嵌套組件的重復(fù)事件綁定,具體可以看下 JavaScript 事件委托一文。
小結(jié)
DOM 非常慢,非常消耗性能。所以 DOM 性能優(yōu)化是前端優(yōu)化非常重要的一環(huán)。下面是主要內(nèi)容:
- 最小化 DOM 的訪問(wèn)次數(shù),盡量將工作交給 JavaScript 去完成。
- 小心 HTML 集合與數(shù)組的差別 —— HTML 集合會(huì)根據(jù) DOM 的改變而發(fā)生改變。數(shù)組的性能優(yōu)于 HTML 集合。
- 使用批量修改的方式減少重排和重繪次數(shù)。
- 使用事件委托減少事件綁定數(shù)量。
最后
由于書(shū)中內(nèi)容太多,講的比較籠統(tǒng)。不過(guò)還是希望能夠?qū)Υ蠹矣兴鶐椭?。如果有什么?wèn)題歡迎留言和我溝通。
最后我有個(gè)疑問(wèn),既然說(shuō)改變布局會(huì)產(chǎn)生重排,那么像 transform + translate 這種變形動(dòng)畫(huà)改變了大小的情況,重排的頻率如何,是否特別消耗性能。相比于不斷的修改 top、left、width 和 height 的性能如何(想必是更高的)?
這個(gè)問(wèn)題之后有空我會(huì)做個(gè)調(diào)研寫(xiě)篇文章分享一下~