前端性能優化--為什么DOM操作慢?


作為一個前端,不能不考慮性能問題。對於大多數前端來說,性能優化的方法可能包括以下這些:

  1. 減少HTTP請求(合並css、js,雪碧圖/base64圖片)
  2. 壓縮(css、js、圖片皆可壓縮)
  3. 樣式表放頭部,腳本放底部
  4. 使用CDN(這部分,不少前端都不用考慮,負責發布的兄弟可能會負責搞好)
  5. 緩存……

等等……

有興趣的同學,可以自行搜索雅虎關於前端優化的十四條規則。但這些規則當中,有多少是需要前端工程師付諸實踐的?就我來說,CDN、緩存的設置,就是不需要我去關心的(作為一個苦逼外包,是沒有權限去生產環境操作的)也就是說,一些優化的方法,可能只是需要前端工程師知道,說得難聽點,就是應付面試,工作中遇不到。反而是一些工作中真正遇到的難題,面試中卻很少被問到(純屬個人經驗)。有些問題,同行們(包括我)可能稀里糊塗地用正確的方式解決了,但實際上,卻並不知道自己為什么這樣做。

books

以上三本書,有幸拜讀。說不上醍醐灌頂,至少是讓我認識到了差距。

我打算把自己的收獲記錄下來,希望對大家有所幫助。文中可能出現錯誤,希望大家能盡量用友善的語氣提出來,謝謝!(づ ̄ 3 ̄)づ

不僅要避免去操作DOM,還要減少去訪問DOM的次數。

在瀏覽器中,DOM和JS的實現,用的並不是同一個“東西”。比如說,我們最熟悉的chrome,JS引擎是V8,而DOM和渲染,靠的是WebCore庫。也就是說,DOM和JS是兩個獨立的個體。

把DOM和JavaScript各自想象成一個島嶼,它們之間用收費橋梁連接。
                                                        --《高性能JavaScript》

1.添加頁面元素,innerHTML vs DOM方法。

document.getElementById('test').innerHTML='<div>test</div>';
var t=document.createElement('div');
t.appendChild(document.createTextNode('test'));
document.getElementById('test').appendChild(t);

以上分別使用兩種方法,向id='test'的元素中添加一個div。之前,大家可能一直被灌輸的思想是innerHTML更快一些,真的是這樣么?

還真是,至少在IE中是這樣,但在基於webkit的新版瀏覽器中,使用DOM方法會稍快一些。所以,到底使用哪一種方法,還是應該有點爭議的。我個人是喜歡innerHTML,因為用起來更簡單。

此外,當需要添加大量相同的元素時,cloneNode比直接創建元素,稍微快一點。

2.訪問元素的正確方法。

2.1 遍歷集合vs遍歷數組

當我們使用document.getElementsByName、document.getElementsByTagName、document.getElementsByClassName、docuemnt.images等方式來獲取DOM元素時,我們得到的是一個HTML集合,這個集合始終與底層文檔保持連接,每次去獲取集合的信息時,都會重復執行一次查詢。

var divs=document.getElementByTagName('div');
for(var i=0;i<divs.length;i++){
  document.body.append(document.createElement('div'))  
}

如果不去運行,我們可能以為上面的代碼會新添加幾個div元素在頁面中,但實際上,因為每次添加完一個div后,divs.length都會被更新(加一),所以,這個循環永遠不會停止。解決辦法非常簡單

var divs=document.getElementByTagName('div');
for(var i=0,len=divs.length;i<len;i++){
  document.body.append(document.createElement('div'))  
}

另外,HTML集合並不是一個數組,如果我們需要對這個集合進行遍歷,可以先把它拷貝進一個數組,這樣再遍歷的時候,效率更高。

function toArray(coll){
  for(var i=0,a=[],len=coll.length;i<len;i++){
      a[i]=coll[i]  
   }
  return a;  
}

2.2訪問元素屬性

當遍歷一個集合時,length屬性應被緩存在循環外部,能夠避免2.1中的邏輯錯誤;集合存儲在局部變量中,也能夠提高效率。此外,當對同一個DOM元素的屬性進行訪問時,把這個DOM緩存成一個局部變量,是更好的選擇。

//只是做演示,真實情況中,當然沒有這樣的需求
//最差的方式
function fo1(){
var name='';
  for(var i=0;i<document.getElementsByTagName('div');i++){
    name=document.getElementsByTagName('div').nodeName;
    name=document.getElementsByTagName('div').nodeType;
   }      
  return name;  
}
//好一點的方式
function fo2(){
var name='';
var coll=document.getElementsByTagName('div');
for(var i=0,len=coll.length;i<len;i++){
  name=coll[i].nodeName;
  name=coll[i].nodeType;    
}
return name;
}
//更好的方式
function fo3(){
  var name='';
  var coll=document.getElementsByTagName('div');
  var ele=null;
  for(var i=0,len=coll.length;i<len;i++){
    el=coll[i];
     name=el.nodeName;
     name=el.nodeType;      
   }        
}

3.選擇器

前面已經提到,document.getElementsByName、document.getElementsByTagName、document.getElementsByClassName、docuemnt.images等方式,獲取到的是HTML集合,效率低下;而querySelector以及querySelectorAll與之相比,得到的是一個NodeList,它是一個類數組對象,不會帶來HTML集合的問題。而且,這個API在獲取元素時,更加方便。唯一的問題,是要考慮目標瀏覽器是否提供支持。

4.重繪和重排

4.1何時重繪、重排?

重繪並不一定導致重排,比如修改某個元素的顏色,只會導致重繪;而重排之后,瀏覽器需要重新繪制受重排影響的部分。導致重排的原因有:

  • 添加或刪除DOM元素
  • 元素位置、大小、內容改變
  • 瀏覽器窗口大小改變
  • 滾動條出現

因為重排和重繪的操作十分昂貴,瀏覽器會通過隊列化修改並批量執行的方式,來進行優化(我的理解是,瀏覽器通過隊列化和批量執行的方式,減少了重繪的次數)。比如:

//這段代碼,並不會去重繪三次
var bodyStyle=document.body.style;
bodyStyle.color='red';
bodyStyle.color='black';
bodyStyle.color='green';

獲取布局的操作,會導致隊列刷新,瀏覽器的優化效果也就沒有了。要避免在布局信息改變時,獲取下列屬性:

  • offsetTop,offsetLeft,offsetWidth,offsetHeight;
  • scrollTop,scrollLeft,scrollWidth,scrollHeight;
  • clientTop,clientLeft,clientWidth,clientHeight;
  • getComputedStyle()/currentStyle

4.2 最小化重排、重繪的建議

建議:不要再修改布局信息的時候,去查詢布局信息

var computed;
var tmp='';
var bodyStyle=document.body.style;
if(document.body.currentStyle){
  computed=document.body.currentStyle  
}else{
  computed=document.defaultView.getComputedStyle(document.body,'')
}
//bad
bodyStyle.color='red';
tmp=computed.backgroundColor;
bodyStyle.color='green';
tmp=computed.backgroundImage;
//good
bodyStyle.color='red';
bodyStyle.color='green';
tmp=computed.backgroundColor;
tmp=computed.backgroundImage;

修改一個元素的多個style時,一次性修改,而不是多次(雖然多次修改,經過現代瀏覽器的優化,也只會導致一次重排,但在老舊的瀏覽器中,仍然會導致多次)。建議:能用css的class解決的,就盡量不用內聯樣式。

:hover會降低響應速度,在處理很大的列表時,避免使用。

5事件委托

每綁定一個事件處理器,都是有代價的。如果有大量的元素需要綁定時間,嘗試使用事件委托。分三步

  • 判斷事件來源
  • 根據不同來源,進行不同操作
  • 取消冒泡,阻止默認行為(可選)
 document.querySelector('#nav').onclick=function (e) {
            if (e.target.nodeName=='A'){
                foo();
            }else{
                foo2()
            }
}

總結:

  • 減少DOM訪問次數
  • 多次訪問同一DOM,應該用局部變量緩存該DOM
  • 盡可能使用querySelector,而不是使用獲取HTML集合的API
  • 注意重排和重繪
  • 使用事件委托,減少綁定事件的數量
  • 更多內容,可以閱讀《高性能JavaScript》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM