記錄一下前端效能優化-為何操作DOM會變慢?
對於大多數前端來說,效能優化的方法可能包括以下這些:
- 減少HTTP請求(合併css、js,雪碧圖/base64圖片)
- 壓縮(css、js、圖片皆可壓縮)
- 樣式表放頭部,指令碼放底部
- 使用CDN(這部分,不少前端都不用考慮,負責釋出的兄弟可能會負責搞好)
- 快取……
不僅要避免去操作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》