前端優化-Javascript篇(4.DOM優化)

上篇我介紹了Javascript標識符查找方面的優化,能夠看出在這方面的優化給性能帶來的提高並不明顯,甚至能夠說基本沒有影響。可是,我今天要分享的是前端Javascript優化的一個大頭。衆所周知,在瀏覽器端Javascript中DOM操做相比普通Javascript代碼來講是比較耗時的,因此在DOM優化上下功夫能夠收到至關可觀的性能優化。下面我將分享幾個DOM方面的性能優化策略。javascript

耗時的DOM操做

瀏覽器中的Javascript能夠分爲兩個部分:ECMAScript和DOM API。而相比原生的ECMAScript來講,DOM API會耗時不少。咱們能夠把這兩部分想象成兩個經過橋樑鏈接的小島,在ECMAScript小島上進行的操做運行速度比在DOM小島上面的操做要快不少,每次在進行DOM操做的時候你都須要從ECMAScript這個小島經過這個橋樑到達DOM小島上而後在上面進行耗時的操做。因此大量的DOM操做就會下降性能。
你們先看看下面這個例子:css

//優化前
var start = new Date().getTime() ;
for(var i = 0 ; i < length ; i ++){
    document.getElementById("test").innerHTML += "a" ;
}
console.log("Before:" + (new Date().getTime() - start)) ;
//優化後
start = new Date().getTime() ;
var content = "" ;
for(var i = 0 ; i < length ; i ++){
    content += "a" ;
}
document.getElementById("test").innerHTML += content ;
console.log("After:" + (new Date().getTime() - start)) ;

從運行結果來看,能夠說差距那是至關明顯啊:前端

請輸入圖片描述

優化前的代碼每一次循環都進行了DOM操做,而優化以後,只在最後一步進行了DOM操做,這就是DOM優化的力量啊。因此,咱們應該在操做的時候儘可能避免對DOM的操做,能少操做DOM就少操做。按照上面的比喻就比如是,咱們經過橋樑從ECMAScript小島到達DOM小島,而後找出須要進行操做的元素,把它再帶回到ECMAScript小島進行操做,經過這個方式,能夠加快操做的速度,咱們應該儘量多的把元素帶回到ECMAScript小島進行操做。java

innerHTML仍是createElement

在頁面上動態添加結點通常有兩個方法:innerHTML和createElement方法。這兩個方法在性能上也有一點差異,具體差異在哪兒呢?上代碼:git

var start = new Date().getTime() ;

var content = "<div>" ;
for(var i = 0 ; i < 1000 ; i ++){
    content += "<div></div>" ;
}
content += "</div>" ;
document.getElementById("test").innerHTML += content ;

console.log("innerHTML:" + (new Date().getTime() - start)) ;

document.getElementById("test").innerHTML = "" ;

start = new Date().getTime() ;
//爲了不直接往test節點上面添加節點引發的頁面重畫,因此使用一個div節點來存儲添加的節點,最後把div添加到頁面中
var div = document.createElement("div") ;
for(var i = 0 ; i < 1000 ; i ++){
    div.appendChild(document.createElement("div")) ;
}
document.getElementById("test").appendChild(div) ;

console.log("createElement:" + (new Date().getTime() - start)) ;

這段代碼在不一樣瀏覽器上的運行結果是不同的:github

請輸入圖片描述

在Chrome上createElement比innerHTML快,而在Firefoxhe和IE上結果則相反,從結果上看彷佛是innerHTML以2:1贏了,但是我仍是建議你們使用createElement,我把上面的代碼改爲下面這樣:segmentfault

var start = new Date().getTime() ;
var test = document.getElementById("test") ;

for(var i = 0 ; i < 1000 ; i ++){
    test.innerHTML += "<div></div>" ;
}

console.log("innerHTML:" + (new Date().getTime() - start)) ;
document.getElementById("test").innerHTML = "" ;

start = new Date().getTime() ;

for(var i = 0 ; i < 1000 ; i ++){
    test.appendChild(document.createElement("div")) ;
}

console.log("createElement:" + (new Date().getTime() - start)) ;

上面這段代碼的運行結果數組

請輸入圖片描述

能夠看出來innerHTML和createElement差不少。爲了測試我用了比較大的數據1000,在實際開發中通常不會出現這種狀況,因此性能上的差別也就不會那麼明顯,可是除了考慮性能問題之外,咱們還應該考慮代碼的可讀性以及可維護下方面的問題,而考慮到這些的話,我我的仍是比較推薦使用createElement,若是你們有什麼別的見解,歡迎一塊兒討論。瀏覽器

HTMLCollection

HTMLCollection是若干個DOM節點的集合,它具備數組的一些特性,好比length屬性、經過下標訪問,可是它並非數組,它沒有push和slice方法。在DOM操做中咱們常常會用到HTMLCollection,下面的方法都會返回HTMLCollection:緩存

  • getElementsByName

  • getElementsByTagName

  • getElementsByClassName

  • document.forms

  • document.images

  • document.links

還有一些別的方法和屬性會返回HTMLCollection,在這裏就不一一列舉了。如何處理它們也是影響性能的一個方面。優化策略跟上面的大同小異,就是用局部變量緩存集合以及集合的長度,我就不進行實際測試了。HTMLCollection還有一個很重要的特性就是它是根據頁面的狀況動態更新的,若是你更新的頁面那麼它的內容也會發生變化。好比下面這段代碼:

var divs = document.getElementsByTagName("div") ;
for(var i = 0 ; i < divs.length ; i ++){
    document.body.appendChild(document.createElement("div")) ;
}

這段代碼的原意是向body中添加多一倍的div節點,可是真正的運行會致使死循環,這就是由於divs是動態更新的,每次向body中添加div節點都會使length屬性發生變化也就是加1,因此這個循環會一直執行下去,在開發的時候應該注意這個問題。一個理想的辦法就是緩存divs的長度,這樣就不會引發死循環了。

節點篩選

若是須要獲得某個節點的因此孩子節點,咱們可能會用到childNodes屬性;獲得第一個孩子,咱們可能會用到firstChild;獲得下一個兄弟節點,咱們可能會用到nextSibling。可是這些屬性都存在一些問題就是它們會把一些空格和空行也看成孩子節點返回給咱們,而這些常常不是咱們所想要的,若是使用這些屬性那麼咱們就須要對它們進行篩選,這樣勢必會影響效率。因此咱們應該用別的屬性來替代這些,看下錶:

請輸入圖片描述

表格左邊的是推薦的屬性,它們只會返回Element節點。不過並非全部瀏覽器都支持,因此在使用以前咱們須要先判斷一下。

使用選擇器方法替代傳統方法

現代瀏覽器給咱們提供了另一種方法在獲取咱們須要的節點,這個方法是querySelectorAll和querySelector。它們經過CSS選擇器做爲參數,返回知足條件的節點。querySelectorAll方法返回知足條件的全部節點而querySelector返回知足條件的第一個節點。使用這兩個方法來替代咱們之前常常用的getElementById,getElementsByTagName等方法也是提升性能的一個途徑。不過仍是老問題,並非全部瀏覽器都支持這兩個方法,全部仍是先作個判斷吧。

Reflow 和 Repaint

首先,Repaint是指頁面上的元素的外觀發生了改變可是不影響佈局的狀況下引發的瀏覽器從新繪畫元素外觀的行爲,好比修改color,background-color等屬性。Reflow是指頁面上的元素的大小布局發生的變化從而引發瀏覽器對頁面其餘元素位置大小進行從新計算而且佈局的行爲。Reflow所致使的性能消耗遠比Repaint大,因此咱們下面重點討論Reflow狀況下的優化策略。
在討論Reflow以前先簡單的看一下瀏覽器加載頁面的過程。以下圖:

請輸入圖片描述

瀏覽器在收到HTML文檔以後對其進行解析,解析過程分爲兩個部分DOM文檔的解析和CSS樣式的解析。解析DOM文檔生成一個DOM樹,DOM樹和解析出來的CSS樣式組合生成一個渲染樹,最後瀏覽器根據這個渲染樹進行頁面的排版和繪畫。而最後這一步就是會涉及到Reflow和Repaint。
如下這幾個行爲會引發頁面的Reflow或Repaint:

  1. 添加,刪除,更新DOM節點

  2. 隱藏/顯示DOM節點(display:none或visibility:hidden)

  3. 修改樣式

  4. 改變窗口大小,滾動頁面

其實瀏覽器在這方面已經幫咱們作了一些優化了,對於每一個觸發Reflow的行爲瀏覽器並不會立刻就觸發,而是把它們保存在一個隊列中,當到達必定數量的時候再進行批量的Reflow,這樣就不須要每次都進行Reflow。可是,咱們的一些行爲會影響到瀏覽器的優化,使得Reflow立刻觸發。當咱們請求下面這些屬性的時候發生這種現象:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight

  2. scrollTop/Left/Width/Height

  3. clientTop/Left/Width/Height

  4. getComputedStyle(), or currentStyle(IE)

每當咱們請求這些屬性時,瀏覽器爲了返回實時的狀況就必須立刻進行Reflow以計算出咱們所須要的屬性。因此咱們應該儘可能少的使用這些屬性。
從上面能夠發現,基於全部DOM操做都會引發Reflow或Repaint,因此儘量避免頁面的Reflow或Repaint能夠很好的提升DOM性能。那麼該怎麼作才能最好的避免或最小化Reflow呢?下面有幾個有用的建議:
1.不要逐一修改樣式,而改成經過修改className來批量改變樣式,若是樣式須要動態計算,那麼也要使用cssText屬性來批量添加樣式。例如:

// 錯誤的作法
var left = 10,
    top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";
 
// 使用修改className來進行優化
el.className += " theclassname";
 
// 若是須要動態修改css,那麼就使用cssText
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";

2.批量處理DOM操做而且讓元素脫離文檔流,等操做結束後再放回文檔流中。有如下幾種辦法:

  • 使用display:none隱藏element,而後進行操做,最後再顯示出來

  • 使用documentFragment ,把新增的節點放在documentFragment中,最後再把documentFragment放到DOM中,由於把documentFragment放到DOM中,它只會把它的孩子節點放到DOM中,就好像documentFragment不存在。

  • 經過cloneNode複製節點,而後離線進行操做,最後再替換DOM中的節點。

3.儘可能少的訪問會引發立刻Reflow的屬性,使用局部變量來緩存這些屬性,好比:

var left = el.offsetLeft,
    top  = el.offsetTop
    esty = el.style;
for(big; loop; here) {
    left += 10;
    top  += 10;
    esty.left = left + "px";
    esty.top  = top  + "px";
}

4.對於須要動畫的元素,儘可能讓它脫離文檔流,這樣就能儘可能引發儘可能小的Reflow

5.儘可能少使用table佈局

事件代理

事件代理我想這個你們應該都知道了。越多的事件綁定頁面就加載越慢而且佔用更多內存,同時綁定太多事件也會使得代碼的可讀性下降。使用事件代理的方法原理就是把事件綁定到元素的父節點,而後在處理函數中判斷target,根據不一樣的target執行不一樣的邏輯。這樣能很大程度的減小綁定是事件數量而且提升代碼的簡潔度。

總結

看了這麼多其實總結起來仍是比較簡單的,在進行DOM操做的時候儘可能把DOM操做轉換爲本地的Javascript操做,使用時先緩存一些DOM元素或者屬性,緩存長度。在須要進行大量DOM操做的時候,先讓元素脫離文檔,等操做結束再把元素放回文檔中。優化策略仍是須要在實踐中不斷嘗試,不斷摸索,找出最優的解決方案。

最近準備畢設沒什麼時間更新博客,後面儘可能安排好時間作到一週一篇,前端優化Javascript篇未完待續。。。

原文地址:
http://lakb248.github.io/2014/06/13/optimization_of_front-end--javascript(4optimization_of_dom)/

最後,安利下個人我的博客,歡迎訪問: http://bin-playground.top

相關文章
相關標籤/搜索