摘要:訪問集合元素時使用局部變量對于任何類型的訪問,如果對同一個屬性或者方法訪問多次,最好使用一個局部變量對此成員進行緩存。
三、DOM Scripting DOM編程
我們都知道對DOM操作的代價昂貴,這往往成為網頁應用中的性能瓶頸。在解決這個問題之前,我們需要先知道什么是DOM,為什么他會很慢。
DOM in the Browser World 瀏覽器中的DOMDOM是一個獨立于語言的,使用XML和HTML文檔操作的應用程序接口(API)。瀏覽器中多與HTML文檔打交道,DOM APIs也多用于訪問文檔中的數據。而在瀏覽器的實現中,往往要求DOM和JavaScript相互獨立。例如在IE中,JavaScript的實現位于庫文件jscript.dll中,而DOM的實現位于另一個庫mshtml.dll中(內部代號Trident),這也是為什么IE內核是Trident,IE前綴為-ms-,當然,我們說的瀏覽器內核其實英文名叫 Rendering Engine/Layout Engine,準確翻譯應該是渲染引擎/排版引擎/模板引擎(其實是一個東西);另外一個就是JavaScript引擎,ie6-8采用的是JScript引擎,ie9采用的是Chakra。而這是相分離的;Chrome中Webkit的渲染引擎和V8的JavaScript引擎,Firefox中Spider-Monkey的JavaScript引擎和Gecko的渲染引擎,都是相互分離的。
Inherently Slow 天生就慢
因為上文所說的,瀏覽器渲染引擎和JavaScript引擎是相互獨立的,那么兩者之間以功能接口相互連接就會帶來性能損耗。曾有人把DOM和ECMAScript(JavaScript)比喻成兩個島嶼,之間以一座收費橋連接,每次ECMAScript需要訪問DOM時,都需要過橋,交一次“過橋費”,操作DOM的次數越多,費用就越高,這里的費用我們可以看作性能消耗。因此請盡力減少操作DOM的次數。
1. DOM Access and Modification DOM訪問和修改
訪問DOM的代價昂貴,修改DOM的代價可能更貴,因為修改會導致瀏覽器重新計算頁面的幾何變化,更更更貴的是采用循環訪問或者修改元素,特別是在HTML集合中進行循環。簡單舉例:
function innerHTMLLoop(){ for ( var count = 0; count < 100000; count++){ document.getElementById("p").innerHTML += "-"; } }
這時候,每執行一次for循環,就對DOM進行了一次讀操作和寫操作(訪問和修改);此時我們可以采用另外一種方式:
function innerHTMLLoop(){ var content = ""; for ( var count = 0; count < 100000; count++){ content += "-"; } document.getElementById("p").innerHTML += content; }
我們使用了一個局部變量存儲更新后的內容,在循環結束時一次性寫入,這時候只執行了一次讀操作和寫操作,性能提升顯著。因此,盡量少的操作DOM,如果可以在JavaScript范圍內完成的話。
2. innerHTML Versus DOM methods innerHTML與DOM方法
在老版本瀏覽器中,innerHTML更快但差別不大,更新的瀏覽器中,不相上下,最新的瀏覽器中,DOM方法更快,但依然差別不大。
3. Cloning Nodes 節點克隆
這樣的方法和DOM方法操作速度不相上下。
HTMLCollection是用于存放DOM節點引用的類數組對象。得到的方法有:document.getElementByName/document.getElementByClassName/document.getElementByTagName/document.querySelectAll/document.images/document.links/document.forms等;也有類似于document.forms[0].elements(頁面第一個表單的所有字段)等。
上面這些方法返回HTMLCollection對象,是一種類似數組的列表,沒有數組的方法,但是有lenth屬性。在DOM標準中定義為:“虛擬存在,意味著當底層文檔更新時,他們將自動更新”。HTML集合實際上會去查詢文檔,更新信息時,每次都要重復執行這種查詢操作,這正是低效率的來源。
Expensive collections 昂貴的集合
先看個例子:
var oDiv = document.getElementByTagName("div"); for (var i = 0; i < oDiv.length; i++){ document.body.appendChild(document.createElement("div")) }
好吧,這是個死循環,永遠不會結束,但是這個過程中,每訪問一次oDiv.length,就會重新計算一遍其長度,當然,前提是對所有的div重新進行一次遍歷,因此,在這里,我們最好使用一個變量暫存oDIv.length:
var oDivLen = document.getElementByTagName("div").length; for (var i = 0; i < oDivLen; i++){ document.body.appendChild(document.createElement("div")) }
從性能角度來講,這樣做會快很多。同時,因為對HTML集合的訪問比對數組訪問要更耗費性能,因此在某些不得不多次訪問HTML集合的情況下,可以先將集合存儲為一個數組,然后對數組進行訪問:
function toArray(htmlList){ for (var i = 0, htmlArray = [], len = htmlList.length; i < len; i++){ htmlArray[i] = htmlList[i]; } return htmlArray; }
當然,這也需要額外的開銷,需要自己進行權衡是否有必要這樣做。
Local variables when accessing collection elements 訪問集合元素時使用局部變量
對于任何類型的DOM訪問,如果對同一個DOM屬性或者方法訪問多次,最好使用一個局部變量對此DOM成員進行緩存。特別是在HTML集合中訪問元素時,如果多次對集合中的某一元素訪問,同樣需要將這個元素先進行緩存。
DOM API提供了多種訪問文檔結構特定部分的方法,去選擇最有效的API。
Crawling the DOM 抓取DOM
如果你可以通過:document.getElementByID();獲得某元素就不要去用document.getElementById().parentNode;這么麻煩去獲取。如果你可已通過nextSibling去獲取一個元素就不要通過childNodes去獲取,因為后者是一個nodeList集合。
Element nodes 元素節點
DOM包含三個節點(也可以說是四個):元素節點、屬性節點、文本節點(以及注釋節點);通常情況下,我們獲取到和使用的是元素節點,但是我們通過childNodes、firstChild、nextSibling等方法獲取到的是所有節點的屬性,js中有一些其他的API可以用來只返回元素節點或者元素節點的某些屬性,我們可以用這些API取代那些返回整個全部節點或者節點屬性的API,例如:
childNodeschildrenchildNodes.lengthchildElementCountfirstChildfirstElementChildlastChildlastElementChildnextSiblingnextElementSiblingpreviousSiblingpreviousElementSibling
在所有的瀏覽器中,后者比前者要快,只不過IE中后面部分方法并不支持,比如IE6/7/8,只支持children方法。
The Selectors API 選擇器API
傳統的選擇器在性能方面問題不大,只不過應用場景相對單一,當我們用習慣了CSS選擇器之后,我們會覺得DOM給我們提供的選擇器讓我們抓狂。在querySelector/querySelectorAll之前,如果我們想要查找到元素下符合條件的另一元素時,不得不使用類似下面的方法:document.getElementById("id1").getElementById("id2");,但如果你想獲取一個類名為class1或類名為class2的div的時候,不得不這么處理:
function getDivClass1(className1,className2){ var results = []; divs = document.getElementByTagName("div"); for (var i = 0,len = divs.length; i < len; i++){ _className = divs[i].className; if(_className === className1 || _className === className2){ results.push(divs[i]); } } return results; }
不僅僅是因為冗長的代碼,多次對DOM進行遍歷帶來的性能問題也不可小窺;不過在有了querySelector/querySelectorAll之后,這一切變得簡單,減少對DOM的遍歷也帶來了性能的提升。上面兩個例子可以重寫如下:
document.querySelector("#id1 #id2"); document.querySelectorAll("div.className1,div.className2");
因此,如果可以,盡量使用querySelector/querySelectorAll吧。
Repaints and Reflows 重繪和重排(也稱回流)這涉及到一個比較古老的議題,瀏覽器在拿到服務器響應時都干了什么。我查閱了相當一部分資料(網上很多地方說法是不準確的,包括一些問答、博客),去了解整個流程,這里簡單的描述一下過程。更多細節可參考《瀏覽器的工作原理:新式網絡瀏覽器幕后揭秘》,原版地址(http://taligarsiel.com/Projec...)。
上文提到過,瀏覽器的實現一般包括渲染引擎和JavaScript引擎;二者是相互獨立的。
我們先從渲染引擎的角度來看一下在拿到服務器的文檔后的處理流程:
Parsing HTML to construct the DOM tree 解析HTML以構建DOM tree
解析HTML文檔,將各個標記逐個轉化為DOM tree 上的DOM節點;當然并不是一一對應;類似于head這樣的標記是在DOM tree上是沒有對應的節點的。在這個過程中,同時被解析的還包括外部CSS文件以及樣式元素中的樣式數據,這些數據信息被準備好進行下一步工作。
Render tree construction 構建render tree
在DOM tree構建過程中,CSS文件同時被解析,DOM tree上每一個節點的對應的顏色尺寸等信息被保存在另一個被稱作rules tree的對象中(具體實現方式webkit和gecho是不一樣的,可參考上文提到過的《瀏覽器的工作原理》)。DOM tree和rules tree兩者一一對應,均構建完成之后,render tree也就構建完成了。
Layout of the render tree 布局render tree
依據render tree中的節點信息和對應的rules中的尺寸信息(包括display屬性等),為每一個節點分配一個應該出現在屏幕上的確切坐標。
Painting the render tree 繪制render tree
就是將已布局好的節點,加上對應的顏色等信息,繪制在頁面上。
當然,瀏覽器并不會等到全部的HTML文檔信息都拿到之后才進行解析,也不會等到全部解析完畢之后才會進行構建render tree和設置布局。渲染引擎可能在接收到一部分文檔后就開始解析,在解析了一部分文檔后就開始進行構建render tree和layout render tree。
JavaScript引擎的工作:
正常的流程中,渲染引擎在遇到標記時,會停止解析,由JavaScript引擎立即執行腳本,如果是外部則立即進行下載并執行,這期間渲染引擎是不工作的。如果腳本被標注為defer時,腳本立即進行下載,但暫不執行,而且這期間也不會阻塞渲染引擎的渲染,腳本會在文檔解析完畢之后執行;如果腳本被標注為async時,腳本立即進行下載,下載完成后會立即執行,只不過這個過程和渲染時兩個線程進行的,二者是異步執行,同樣不會影響頁面渲染。defer和async二者的區別是:雖然都是立即下載(這兩個都只作用于外部腳本),但是前者是在文檔解析完畢后執行,后者是下載完成后立即執行,因此前者可以保證腳本按照順序執行,而后者誰先下載完誰先執行會導致依賴關系混亂。
注:關于async和defer,在查閱資料后的說法見上文。但在我自己編寫DEMO測試的過程中,發現在Chrome中偶爾會全部阻塞,偶爾都不阻塞,最多的是腳本以上的文檔不阻塞,以下的文檔被阻塞;IE/FireFox中完全不鳥這倆屬性,直接等到腳本執行完畢才出現頁面。
說回重繪和重排,
先說重排,當render tree中的一部分因為元素的尺寸、布局、顯示隱藏等改變而需要重新構建render tree時,此時就叫重排,也就是重新構建render tree;會引起重排的場景包括:添加或者刪除可見的DOM元素、元素位置改變、元素尺寸改變(邊距、填充、邊框、寬高等)、內容改變(因此引起尺寸改變)、頁面渲染初始化(首次加載頁面)、瀏覽器窗口尺寸改變;還有很重要的一點就是在嘗試獲取頁面元素的尺寸時也會強制引發重排(因為重排的過程會重新計算元素的尺寸,所以為保證獲得最新的尺寸信息,會先強制進行重排),例如:offsetTop/offsetLeft/offsetWidth/offsetHeight/scrollTop/scrollLeft/scrollWidth/scrollHeight/clientTop/clientLeft/clientWidth/clientHeight/width/height/getComputedStyle()/currentStyle()。
重繪一般發生在元素的外觀變化時,首先重排一定會引起重繪,當元素的顏色/背景色/透明度/visibility屬性等發生變化也會引起重繪。
Queuing and Flushing Render Tree Changes 查詢并刷新render tree改變
前文我們知道了構建render tree過程中,會先對CSS以及樣式元素中的樣式數據進行解析計算,在引發重排時,會對樣式數據重新計算,性能問題就出現在大量計算的過程中,在大多數的瀏覽器中,會通過隊列化修改和批量顯示優化重排的過程,但是我們剛所提到的嘗試獲取頁面尺寸信息會強制引發重排,類似下面代碼:
var computedValue, tmp = "", bodystyle = document.body.style; if (document.body.currentStyle) { computedValue = document.body.currentStyle; }else{ computedValue = document.defaultView.getComputedStyle(document.body, ""); } bodystyle.color = "red"; tmp = computedValue.backgroundColor; bodystyle.color = "white"; tmp = computedValue.backgroundImage; bodystyle.color = "green"; tmp = computedValue.backgroundAttachment;
上面示例中,body的字體顏色被改變了三次,每次改變后都對body的樣式信息進行了查詢,雖然查詢的信息和字體顏色無關,但是瀏覽器會因此刷新渲染隊列并進行重排,所以共進行了三次重排,也理所當然的進行了三次重繪,這里可以改進一下:
var computedValue, tmp = "", bodystyle = document.body.style; if (document.body.currentStyle) { computedValue = document.body.currentStyle; }else{ computedValue = document.defaultView.getComputedStyle(document.body, ""); } bodystyle.color = "red"; bodystyle.color = "white"; bodystyle.color = "green"; tmp = computedValue.backgroundColor; tmp = computedValue.backgroundImage; tmp = computedValue.backgroundAttachment;
在下面的例子中,實際上引起了一次重排和兩次重繪,首先bodystyle.color的三次變化被批量化一次處理,只進行了一次重繪,接著對computedValue的訪問批量處理,進行了一次重排,接著此次重排又引起一次重繪。速度要比優化之前的更快。因此,盡量不要在布局信息發生變化時對元素尺寸進行查詢。
Minimizing Repaints and Reflows 最小化重繪和重排
上面的例子其實就是減小重繪重排的一種方法,盡量將對DOM和風格改變的操作放在一起,在一次批量修改中完成。
① style changes 改變風格
先舉例:
var oDiv = document.getElementById("div"); oDib.style.borderLeft = "1px"; oDib.style.padding = "10px"; oDib.style.width = "100px";
這里對oDiv進行了三次改變,每次改變都涉及到元素的幾何屬性,雖然大多數瀏覽器進行了優化,只進行一次重排,但部分老式瀏覽器中,效率很低,而且如果在此時進行了布局信息查詢,會導致三次重排的進行,我們可以換一種風格實現:
var oDiv = document.getElementById("div"); oDiv.style.cssText = "border-left: 1px;padding: 10px;width: 100px;";
優化后的代碼,只進行一次重排,但是會覆蓋原有的樣式信息(這里的覆蓋會清空原來所有的行內樣式信息),因此也可以這么寫:
oDiv.style.cssText += ";border-left: 1px;padding: 10px;width: 100px;";
當然,我們也可以通過對類名的修改,類名事先在CSS中定義了對應的樣式信息,來達到修改樣式的需求,比如:
var oDiv = document.getElementById("div"); oDiv.className += "current";
② Batching DOM changes 批量修改DOM
原文翻譯是批量修改DOM,我的理解是批量DOM修改,這種方法是將會被多次修改的DOM元素,先從文檔流摘除,然后批量修改,然后帶回文檔,這樣僅僅在摘除和帶回時發生兩次重排,中間的多次修改,并不會帶來重排。將元素從文檔摘除有很多方法,比如將元素隱藏(dispaly:none;)、DOM之外新建一個文檔片段修改后添加到原節點位置。
③ Caching Layout Information 緩沖布局信息
比如我們通過setTimeout實現一個動畫:元素每10毫秒向右下方移動1px;從100X100移動到500X500:
myElement.style.left = 1 + myElement.offsetLeft + "px"; myElement.style.top = 1 + myElement.offsetTop + "px"; if (myElement.offsetLeft >= 500){ stopAnimation(); }
代碼中,我們每次查詢myElement.offsetLeft/myElement.offsetTop值的時候,都引起了一次頁面重排,一次循環中,至少進行了三次重排,性能糟糕的不要不要的,我們可以通過優化,將myElement.offsetLeft/myElement.offsetTop的值緩存起來:
var current = myElement.offsetLeft;
循環內:
current++; myElement.style.left = current + "px"; myElement.style.top = current + "px"; if (current >= 500) { stopAnimation(); }
將myElement.offsetLeft緩存起來,初始查詢一次,之后不再進行查詢,只引用變量current;而且在瀏覽器的優化后,每次循環只進行了一次重排,性能提升不是一點。
④ Take Elements Out of the Flow for Animations 將元素提出動畫流
顯示/隱藏部分頁面、折疊/展開動畫是很常見的動畫交互模式。很多時候展開/折疊動畫,會將頁面一部分擴大,將下面的部分向下推開,這樣,會帶來不僅僅是展開部分的重排,包括下面的部分全部重排,而重排的性能消耗,和影響的渲染樹程度有關,因此我們可以減少對頁面影響部分來實現減小重排帶來的性能消耗。
(1) 使用絕對坐標定位頁面動畫部分,使其脫離于頁面文檔布局流之外,這樣它的改變對其他文檔的位置尺寸等信息無影響,不會引起其他部分重排
(2) 展開動作只在動畫元素上進行,其下面的元素不隨著動畫元素展開推移,只是被遮蓋,這樣也不會引起大范圍的渲染樹重新計算
(3) 在動畫結束后,再將下面的元素一次性移動,而不是動畫過程中慢慢移動。
⑤ IE and :hover IE和:hover
IE7+全面支持:hover,但是大量元素使用:hover會帶來嚴重的性能問題。比如一個大型的表格,使用tr:hover來使鼠標光標所在行高亮時,會使cpu的使用率提升80%-90%。因此應當盡量避免大量元素使用:hover屬性。
當頁面上存在大量元素,而且每個元素都有一個或者多個事件句柄與之綁定的時候,可能會影響性能,因為掛接每個句柄都是有代價的,更多的頁面標記和JavaScript代碼,運行期需要訪問和修改更多的DOM節點。更重要的是事件掛載發生在onload事件中,而這個時間段是有很多事要處理的,無形中影響到其他事件的處理。但是你給頁面上一百個元素每人綁定了一個點擊事件,但是可能只有十個可能會被真正點擊調用,做了90%的無用功。
我們可以通過事件托管來處理這類需求,原理是事件冒泡,也就是在包裹元素上掛接一個句柄,用于處理其子元素發生的所有事件。比如在點擊時,判斷當前標簽的類名,不同類名執行對應的操作,這樣既不用給每一個元素綁定事件句柄,也實現了每個元素的點擊事件處理。Jquery中的on可以給動態添加的元素綁定事件也是利用了事件托管的辦法。
高性能JavaScript閱讀簡記(一)
高性能JavaScript閱讀簡記(二)
高性能JavaScript閱讀簡記(三)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/84634.html
摘要:對于直接量和局部變量的訪問性能差異微不足道,性能消耗代價高一些的是全局變量數組項對象成員。當一個函數被創建后,作用域鏈中被放入可訪問的對象。同樣會改變作用域鏈,帶來性能問題。 早前閱讀高性能JavaScript一書所做筆記。 一、Loading and Execution 加載和運行 從加載和運行角度優化,源于JavaScript運行會阻塞UI更新,JavaScript腳本的下載、解析...
摘要:對于直接量和局部變量的訪問性能差異微不足道,性能消耗代價高一些的是全局變量數組項對象成員。當一個函數被創建后,作用域鏈中被放入可訪問的對象。同樣會改變作用域鏈,帶來性能問題。 早前閱讀高性能JavaScript一書所做筆記。 一、Loading and Execution 加載和運行 從加載和運行角度優化,源于JavaScript運行會阻塞UI更新,JavaScript腳本的下載、解析...
摘要:訪問集合元素時使用局部變量對于任何類型的訪問,如果對同一個屬性或者方法訪問多次,最好使用一個局部變量對此成員進行緩存。 三、DOM Scripting DOM編程 我們都知道對DOM操作的代價昂貴,這往往成為網頁應用中的性能瓶頸。在解決這個問題之前,我們需要先知道什么是DOM,為什么他會很慢。 DOM in the Browser World 瀏覽器中的DOM DOM是一個獨立于語言...
摘要:移植到中的一個典型的達夫設備的例子為一個很長很長的數組。但是達夫設備最初這種詭異的寫法和思路,還是驚艷了很多人的,值得我們思考。高性能閱讀簡記一高性能閱讀簡記二高性能閱讀簡記三 四、Aligorithms and Flow Control 算法和流程控制 1、Loops 循環 a、避免使用for/in循環在JavaScript標準中,有四種類型循環。for、for/in、while、...
閱讀 3503·2021-11-24 09:39
閱讀 781·2019-08-30 14:22
閱讀 3031·2019-08-30 13:13
閱讀 2310·2019-08-29 17:06
閱讀 2918·2019-08-29 16:22
閱讀 1255·2019-08-29 10:58
閱讀 2427·2019-08-26 13:47
閱讀 1628·2019-08-26 11:39