高性能 JavaScript - DOM

這幾天分享一下我看《高性能 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ù)。大致方案如下:

  1. 將需要修改的元素節(jié)點(diǎn)脫離文檔流。
  2. 修改脫離文檔流的元素節(jié)點(diǎn)。
  3. 將元素帶入文檔流。

具體的方案有三種:

  1. 將需要修改的 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'
  1. 使用 document.createDocumentFragment() 方法創(chuàng)建文檔片段,在文檔片段中定義修改的 DOM,最后將文檔片段應(yīng)用到 DOM 中。
var fragment = document.createDocumentFragment()
appendDataToElement(fragment, data)
document.getElementById('list').appendChild(fragment)
  1. 使用 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ě)篇文章分享一下~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容