前言:
Javascript絕對是最火的編程語言之一,一直具有很大的用戶群,具有廣泛的應用前景。而在前端開發中,它也是三駕馬車之一,並且是最重要的一環。要想給用戶提供更流暢的操作體驗,更友好的交互,對Javascript程序進行優化、提高執行效率也就必不可少。那么我們怎么樣才能編寫出高性能的JS程序呢?本文是在閱讀《高性能網站建設進階指南》和《高性能JavaScript》之后寫的一篇總結,自己也加深一下印象,希望可以幫助大家!
一、數據訪問
1、高效數據存儲
數據在腳本中的存儲位置會影響腳本的執行時間。一般而言有四種方式可以存儲數據:
- 字面量值
- 變量
- 數組元素
- 對象屬性
用不同的方式存儲數據會帶來不同的性能開銷。在大部分瀏覽器中,從字面量中讀取數據和從局部變量中讀取的開銷最小,幾乎可以忽略不計。真正的差異在從數組或對象中讀取數據,經過對以上四種不同數據存儲位置的實驗表明,數組和對象存儲方式性能消耗是大於其他兩種的。
為提高數據存儲效率,我們需要把那些需要頻繁存取的值存儲到局部變量中。
function process(data){ if (data.count>0){ for (var i=0;i<data.count;i++){ processData(data.item[i]); } } } // 把data.count存儲到局部變量,只需要讀取一次即可,其他都在局部變量里讀取 function process(data){ var count = data.count; if (count>0){ for (var i=0;i<count;i++){ processData(data.item[i]); } } }
還有就是隨着數據結構的加深,也會對數據存取帶來負面影響。如data.item.subitem.count要比data.count慢。
在Js程序中使用超過一次的對象屬性或數組元素存儲為局部變量是一種不錯的方法。特別是在處理HTMLCollection對象(如getElementByTagName這樣的DOM方法返回的值或element.childNodes這樣的屬性值)時使用局部變量更加重要,因為每次存取HTMLCollection對象時都會進行動態查詢,也就是說每一次讀取都會重新查詢的,這是一個消耗很大的行為。
2、管理作用域
我們知道js里有作用域鏈的概念,在程序執行時,Javascript引擎會通過搜索上下文的作用域鏈來解析諸如變量和函數名這樣的標識符。其會從作用域鏈的最里面開始檢索,按照由內到外的順序,直到完成查找,一旦完成查找就結束搜索。很明顯如果JS引擎可以直接找到標識符要比向外回溯查找要快不少,也就是說標識符在作用域鏈中的位置越深,查找和訪問需要的時間也就越長;如果作用域鏈管理的不合理,會給腳本的執行帶來負面影響。目前來說,有些瀏覽器已經優化過js引擎,比如chrome和safari4,訪問跨作用域標識符時性能損失不明顯,其他瀏覽器依然有極大的影響。為了使程序在眾多瀏覽器上都能高效運行,我們必須對作用域進行良好的管理,下面是一些具體的方法僅供參考:
- 2.1使用局部變量
局部變量是JS種讀寫最快的標識符,我們應盡可能的使用局部變量。如果在程序中,任何非局部變量在函數中使用次數超過一次,我們最好將其存儲為局部變量來提高速度。例如:
function createChildFor (elementID){ var element = document.getElementById(elementID), newElement=document.createElement("div"); element.appendChild(newElement); } //將document存儲到一個局部變量里
function createChildFor (elementID){ var doc=document, element = doc.getElementById(elementID), newElement=doc.createElement("div"); element.appendChild(newElement); }
原函數是要到全局作用域里查找兩次document,而改寫后的函數只需要查找一次全局作用域,其他的通過查找局部作用域就可以了。 改寫后的函數無疑是會提高速度的,要切記全局對象始終是作用域鏈中的最后一個對象,要盡量避免使用過多的全局變量。
- 2.2增長作用域鏈
在代碼執行時,對應的作用域鏈常常是保持靜態的。然而當遇到with語句和try-catch中的catch時,會改變作用域鏈的。在遇到with語句時,會將對象屬性作為局部變量來顯示,使其便於訪問,也就是說把一個新的對象添加到了作用域鏈的頂端,這樣必然影響對局部標志符的解析。當with語句執行完畢后,會把作用域鏈恢復到原始狀態。在編程中要避免這種情況。執行catch語句時遇到的情況與with語句差不多,然而它只會在捕捉到錯誤時才會執行,對性能的影響要小於with語句。
管理好作用域鏈是一種高效便捷的方法,只需要少量的工作就可以提高性能,在平時編寫JS時要注意這個問題。
二、DOM編程
在瀏覽器內部進行頁面呈現和JS解析的不是一個引擎,DOM為JS操作HTML和XML提供了API,要想把JS對DOM的修改呈現出來,無疑需要頁面呈現引擎和JS引擎進行溝通,兩個相互獨立的功能只要通過接口彼此連接,就會產生消耗。而且它在瀏覽器中是以JavaScript實現的,JS本身的執行效率就低於JAVA或者C(JS是解釋型語言,邊編譯邊執行;JAVA或者C是編譯型語言,一次編譯,直接執行),所以DOM有點慢。可能給程序帶來負面影響的DOM操作,大致可以分為三類:訪問和修改DOM元素,修改DOM元素的樣式導致重繪(repaint)和重排(reflow),通過DOM事件處理與用戶交互。
- 在訪問與修改DOM時開銷很大,特別是修改元素時,會導致瀏覽器重新渲染頁面。所以,要盡量減少修改次數,可以多次累積一次修改。在獲取HTML元素集合時(如,getElementByName(),getElementByClassName(),getElementByTagName()等),也是非常耗能的,因為動態查詢的,隨時反映元素狀態,多次使用元素集合時應緩存為局部變量,以減少查詢次數。還有在遍歷DOM時,最好使用最合適的最高效的API,比如選擇器API(querySelectorAll(),querySelector())已經被很多瀏覽器提供了原生的支持,原生的API在任何時候都要比其他非原生方式快速。
- 重繪與重排開銷也非常大。特別是重排,在DOM的變化影響了元素的幾何屬性時,瀏覽器會重新計算元素的幾何屬性,其他相關元素也會受到影響,受到影響的元素就會被重新構造渲染樹。完成重排之后,瀏覽器會重新繪制受影響的元素到屏幕中,也就是重繪。每次重排都是非常耗能的,瀏覽器通過隊列化修改並批量的執行來優化重排過程。然而,獲取這些布局信息的時候(offsetTop,scrollTop,clientTop等)會導致隊列刷新,也就是立即執行,不能完全的批量執行。所以,在需要獲取這些布局信息的時候,可以放在批量重排之后。總的來說,要想提高性能,可以批量修改樣式、離線操作DOM樹、緩存布局信息減少訪問布局信息的次數等。
- 如果頁面上元素很多,而且很多元素上也綁定有事件處理程序,也會加重瀏覽器負擔。事件綁定越多,由於瀏覽器要跟蹤每個事件處理器,也會占用更多的內存。可以用事件委托,減少大量的事件綁定,減輕瀏覽器的壓力。
至於改進嘛,個人感覺按照現在這個趨勢就行。隨着DOM0、DOM2、DOM3公布,新的API也在不斷增加,雖然這些新的API中有的會帶來性能問題,但給我們提供了極大的便利。其實,隨着硬件水平的快速發展,很多問題將不再是問題。
三、算法和流程控制
代碼的數量並不是衡量一個程序運行快慢的指標,影響性能的最直接因素是代碼的組織方式,以及具體問題的解決方法。
1.循環
循環是編程中最常見的模式之一,死循環和長時間的循環會影響函數的執行效率。在js里有四種類型的循環,for循環、while循環、do-while循環和for-in循環,其中只有for-in循環比其他循環模式要慢。因此,除非你需要迭代的是一個未知數量的對象,否則要避免使用for-in循環。
在選取合適的循環類型之后,可以優化的因素就剩下每次迭代的事務和迭代次數了。我們需要分析每次迭代時,所做的操作,找出可以可以減少工作量的編程方式。關於減少迭代次數方面,可以采用“Duff's Device”循環體展開技術,它可以使得在一次循環中執行多次迭代操作。
2.條件語句
- 2.1快速條件判斷
使用switch語句還是一串if-else語句是很多編程語言里經典問題。
if語句:當這樣的語句較多時,會有很大的開銷,因為語句的執行流越深,需要判斷的條件也就越多。使用大量的if條件語句是一件很糟糕的事情,但可以采用下面的方法來提高整體性能。
- 將條件按頻率降序排列;
- 優化if語句,將條件語句拆分成幾個分支。這樣就會在少量的判斷次數的情況下完成匹配。
- 2.2switch語句
switch簡化了多重條件判斷的結構,並提升了性能。switch具有很強的可讀性,而且很多語言推薦使用它,並不是因為它的本身,而是很多語言的編譯器可以優化switch語句,使它能更快的求值。在JS中,當僅判斷一兩個條件時,if語句常常比switch語句更快;當兩個以上比較簡單的條件時,switch語句往往更快。這是因為大多數情況下,switch語句執行單個條件所需的時間比if語句少,那么當大量判斷時,switch更優秀。
- 2.3數組查詢
當有大量的離散值需要測試時,以上兩種方式是比較低效的,我們可以把需要查詢的值用字面量方式賦值給數組或者對象,把以前的條件查詢變為數組查詢或者對象成員查詢。它有個很大的優點就是:不用書寫任何條件判斷語句,即使候選值增加時,也幾乎不會產生額外的性能開銷。
3.遞歸
遞歸函數的潛在問題是終止條件不明確或缺少終止條件會導致函數長時間運行,並使得用戶界面處於假死狀態。而且,遞歸函數還可能遇到瀏覽器的“調用棧大小限制”。
js引擎支持的遞歸數量與js調用棧大小直接相關,當你使用了太多的遞歸甚至超過了最大棧容量,瀏覽器會報錯。關於調用棧大小限制,只有IE是和系統空閑內存有關,其他瀏覽器是數量固定的。最常見導致棧溢出的原因是不正確的終止條件,因此遞歸模式錯誤的第一步是驗證終止條件。如果終止條件沒有錯誤,那么可能是算法中包含了太多層遞歸,可以改用迭代、Memoization,或者二者結合使用。其中,Memoization是一種避免重復工作的方法,它緩存前一個計算結果供后續計算使用,避免了重復工作。
四、正則表達式
沒有經過合理優化的正則表達式也可能是造成性能瓶頸的重要因素。在一些需要正則表達式來處理字符串的地方,可以采用以下方式來優化表達式。
- 關注如何讓匹配快速失敗:正則表達式慢的原因通常是匹配失敗的過程慢,而不是匹配成功的過程慢;
- 正則表達式以簡單、必需的字元開始:一個正則表達式的起始標記應當盡可能快速地測試並排除明顯不匹配的位置;
- 使用量詞模式,使它們后面的字元互斥:當字符與字元相鄰或者子表達式能夠重疊匹配時,正則表達式嘗試拆解文本的路徑數量增加,為了避免這種情況,盡量具體化匹配模式;
- 減少分支數量,縮小分支范圍:分支可能要求在字符串的每一個位置上測試所有分支選項,可以通過使用字符集和選項組來減少對分支的需求,或者將分支在正則表達式上的位置推后。
- 使用非捕獲組:捕獲組消耗時間和內存來記錄反向引用,並使它保存最新。如果你不需要一個反向引用,可以使用非捕獲組來避免這些開銷,比如用(?:...)來替代(...)。
- 只捕獲感興趣的文本以減少后處理:如果你需要引用匹配的部分,應該采取一切手段捕獲哪些片段,再使用反向引用來處理。
- 暴露必須的字元:幫助正則表達式引擎在查詢優化過程時做出明智的決策,可以嘗試讓它更容易地判斷哪些字元是必須的。
- 使用合適的量詞;
- 把正則表達式賦值給變量並重用它們;
- 將復雜的正則表達式拆分為簡單的片段:避免在一個正則表達式中處理太多的任務。
五、快速響應用戶界面
大多數瀏覽器讓一個單線程共用於執行js和更新用戶界面,也就是說當執行js時是不能更新用戶界面,反之亦然。關於共同執行js和更新用戶界面的進程通常被稱為“瀏覽器UI線程”。UI線程的工作基於一個簡單的隊列系統,任務會保存到隊列中直到進程空閑,一點空閑隊列的下一個任務就被執行。這些任務要么是執行JS,要么執行UI更新,包括重繪和重排。然而,空閑狀態是理想的,因為用戶的所有操作都會立刻觸發UI更新。如果用戶試圖在任務運行期間與頁面交互,不僅沒有及時的UI更新,甚至可能新的UI更新任務都不會被創建並加入隊列。事實上,大多數瀏覽器在JS運行時會停止把新任務加入UI線程的隊列中,也就是說JS任務必須盡快結束,以免給用戶體驗造成不良影響。
對於瀏覽器自身來說會限制js執行時間的,此限制分為:調用棧大小限制,長時間運行腳本限制。當程序超過這些限制時會終止執行。有研究指出,如果界面在100ms內響應用戶輸入,用戶會認為自己在“直接操縱界面中的對象”,超過100ms時用戶會感到自己與界面失去了聯系,不能給用戶及時的反饋。由於當js運行時無法更新UI,所以如果js執行超過100ms的話會給用戶帶來不好的用戶體驗,所以要極力避免js執行超過100ms。
有時候即使用盡了各種辦法,js執行時間依然大於100毫秒,為了提供好的用戶體驗,我們可以用定時器空出時間片段給UI更新,更新完成后再繼續執行程序。除了使用定時器,我們也可以使用HTML5中新提供的Web Workers API,使程序不占用UI線程資源,給用戶友好的交互體驗。
要想寫出高效的JS代碼,以上所述並不是所有的注意事項,還有很多其他的方式使我們的程序更加簡潔優美。個人感覺優化代碼的方式不可窮盡,掌握好原生的js,對原生js加深了解,根據js的特性我們平時多加注意,應該是可以寫出高效的代碼的。還有一些知識點,在這里不能一一列舉,大家想要有細致的了解,最好還是去讀原著。原著寫的非常好,很贊,值得一讀!