https://segmentfault.com/a/1190000003646305
前端性能優化指南
AJAX
優化
緩存
AJAX
:
異步
並不等於即時
。請求使用
GET
:
當使用
XMLHttpRequest
時,而URL長度不到2K
,可以使用GET
請求數據,GET
相比POST
更快速。
POST
類型請求要發送兩個TCP
數據包。
先發送文件頭。
再發送數據。
GET
類型請求只需要發送一個TCP
數據包。
取決於你的
cookie
數量。
COOKIE
專題
減少
COOKIE
的大小。使用無
COOKIE
的域。
比如圖片
CSS
等靜態文件放在靜態資源服務器上並配置單獨域名,客戶端請求靜態文件的時候,減少COOKIE
反覆傳輸時對主域名的影響。
DOM
優化
優化節點修改。
使用
cloneNode
在外部更新節點然後再通過replace
與原始節點互換。var orig = document.getElementById('container');
var clone = orig.cloneNode(true);
var list = ['foo', 'bar', 'baz'];
var content;
for (var i = 0; i < list.length; i++) {
content = document.createTextNode(list[i]);
clone.appendChild(content);
}
orig.parentNode.replaceChild(clone, orig);優化節點添加
多個節點插入操作,即使在外面設置節點的元素和風格再插入,由於多個節點還是會引發多次reflow。
優化的方法是創建
DocumentFragment
,在其中插入節點後再添加到頁面。
如
JQuery
中所有的添加節點的操作如append
,都是最終調用DocumentFragment
來實現的,createSafeFragment(document) {
var list = nodeNames.split( "|" ),
safeFrag = document.createDocumentFragment();
if (safeFrag.createElement) {
while (list.length) { safeFrag.createElement( list.pop(); ); };
};
return safeFrag;};
優化
CSS
樣式轉換。如果需要動態更改CSS樣式,儘量採用觸發reflow次數較少的方式。
如以下代碼逐條更改元素的幾何屬性,理論上會觸發多次
reflow
。element.style.fontWeight = 'bold' ; element.style.marginLeft= '30px' ; element.style.marginRight = '30px' ;
可以通過直接設置元素的
className
直接設置,只會觸發一次reflow
。element.className = 'selectedAnchor' ;
減少
DOM
元素數量
在
console
中執行命令查看DOM
元素數量。`document.getElementsByTagName( '*' ).length`
正常頁面的
DOM
元素數量一般不應該超過1000
。
DOM
元素過多會使DOM
元素查詢效率,樣式表匹配效率降低,是頁面性能最主要的瓶頸之一。
DOM
操作優化。
DOM
操作性能問題主要有以下原因。
DOM
元素過多導致元素定位緩慢。大量的
DOM
接口調用。
JAVASCRIPT
和DOM
之間的交互需要通過函數API
接口來完成,造成延時,尤其是在循環語句中。
DOM
操作觸發頻繁的reflow(layout)
和repaint
。
layout
發生在repaint
之前,所以layout相對來說會造成更多性能損耗。
reflow(layout)
就是計算頁面元素的幾何信息。
repaint
就是繪製頁面元素。對
DOM
進行操作會導致瀏覽器執行迴流reflow
。解決方案。
純
JAVASCRIPT
執行時間是很短的。最小化
DOM
訪問次數,儘可能在js端執行。如果需要多次訪問某個
DOM
節點,請使用局部變量存儲對它的引用。謹慎處理
HTML
集合(HTML
集合實時連繫底層文檔),把集合的長度緩存到一個變量中,並在迭代中使用它,如果需要經常操作集合,建議把它拷貝到一個數組中。如果可能的話,使用速度更快的API,比如
querySelectorAll
和firstElementChild
。要留意重繪和重排。
批量修改樣式時,
離線
操作DOM
樹。使用緩存,並減少訪問佈局的次數。
動畫中使用絕對定位,使用拖放代理。
使用事件委託來減少事件處理器的數量。
優化
DOM
交互在
JAVASCRIPT
中,DOM
操作和交互要消耗大量時間,因爲它們往往需要重新渲染整個頁面或者某一個部分。
最小化
現場更新
。
當需要訪問的
DOM
部分已經已經被渲染爲頁面中的一部分,那麼DOM
操作和交互的過程就是再進行一次現場更新
。
現場更新
是需要針對現場
(相關顯示頁面的部分結構)立即進行更新,每一個更改(不管是插入單個字符還是移除整個片段),都有一個性能損耗。現場更新進行的越多,代碼完成執行所花的時間也越長。
多使用
innerHTML
。
有兩種在頁面上創建
DOM
節點的方法:
使用諸如
createElement()
和appendChild()
之類的DOM
方法。使用
innerHTML
。
當使用
innerHTML
設置爲某個值時,後臺會創建一個HTML
解釋器,然後使用內部的DOM
調用來創建DOM
結構,而非基於JAVASCRIPT
的DOM
調用。由於內部方法是編譯好的而非解釋執行,故執行的更快。對於小的
DOM
更改,兩者效率差不多,但對於大的DOM
更改,innerHTML
要比標準的DOM
方法創建同樣的DOM
結構快得多。迴流
reflow
。
發生場景。
改變窗體大小。
更改字體。
添加移除stylesheet塊。
內容改變哪怕是輸入框輸入文字。
CSS虛類被觸發如 :hover。
更改元素的className。
當對DOM節點執行新增或者刪除操作或內容更改時。
動態設置一個style樣式時(比如element.style.width="10px")。
當獲取一個必須經過計算的尺寸值時,比如訪問offsetWidth、clientHeight或者其他需要經過計算的CSS值。
解決問題的關鍵,就是限制通過DOM操作所引發迴流的次數。
在對當前DOM進行操作之前,儘可能多的做一些準備工作,保證N次創建,1次寫入。
在對DOM操作之前,把要操作的元素,先從當前DOM結構中刪除:
通過removeChild()或者replaceChild()實現真正意義上的刪除。
設置該元素的display樣式爲“none”。
每次修改元素的style屬性都會觸發迴流操作。
element.style.backgroundColor = "blue";
使用更改
className
的方式替換style.xxx=xxx
的方式。使用
style.cssText = '';
一次寫入樣式。避免設置過多的行內樣式。
添加的結構外元素儘量設置它們的位置爲
fixed
或absolute
。避免使用表格來佈局。
避免在
CSS
中使用JavaScript expressions(IE only)
。將獲取的
DOM
數據緩存起來。這種方法,對獲取那些會觸發迴流操作的屬性(比如offsetWidth
等)尤爲重要。當對HTMLCollection對象進行操作時,應該將訪問的次數儘可能的降至最低,最簡單的,你可以將length屬性緩存在一個本地變量中,這樣就能大幅度的提高循環的效率。
eval優化
避免
eval
:
eval
會在時間方面帶來一些效率,但也有很多缺點。
eval
會導致代碼看起來更髒。
eval
會需要消耗大量時間。
eval
會逃過大多數壓縮工具的壓縮。
HTML
優化
插入
HTML
。
JavaScript
中使用document.write
生成頁面內容會效率較低,可以找一個容器元素,比如指定一個div
,並使用innerHTML
來將HTML
代碼插入到頁面中。避免空的
src
和href
。
當
link
標籤的href
屬性爲空、script
標籤的src
屬性爲空的時候,瀏覽器渲染的時候會把當前頁面的URL
作爲它們的屬性值,從而把頁面的內容加載進來作爲它們的值。爲文件頭指定
Expires
。
使內容具有緩存性,避免了接下來的頁面訪問中不必要的HTTP請求。
重構HTML,把重要內容的優先級提高。
Post-load(次要加載)不是必須的資源。
利用預加載優化資源。
合理架構,使DOM結構儘量簡單。
利用
LocalStorage
合理緩存資源。儘量避免CSS表達式和濾鏡。
嘗試使用defer方式加載Js腳本。
新特性:will-change,把即將發生的改變預先告訴瀏覽器。
新特性Beacon,不堵塞隊列的異步數據發送。
不同之處:網絡緩慢,緩存更小,不令人滿意的瀏覽器處理機制。
儘量多地緩存文件。
使用HTML5 Web Workers來允許多線程工作。
爲不同的Viewports設置不同大小的Content。
正確設置可Tap的目標的大小。
使用響應式圖片。
支持新接口協議(如HTTP2)。
未來的緩存離線機制:Service Workers。
未來的資源優化Resource Hints(preconnect, preload, 和prerender)。
使用Server-sent Events。
設置一個Meta Viewport。
JIT
與GC
優化
untyped
(無類型)。
JAVASCRIPT
是個無類型的語言,這導致瞭如x=y+z
這種表達式可以有很多含義。
y
,z
是數字,則+
表示加法。
y
,z
是字符串,則+
表示字符串連接。而JS引擎內部則使用“
細粒度
”的類型,比如:
32-bit* integer。
64-bit* floating-point。
這就要求js類型-js引擎類型,需要做“boxed/unboxed(裝箱/解箱)”,在處理一次
x=y+z
這種計算,需要經過的步驟如下。
從內存,讀取
x=y+z
的操作符。從內存,讀取
y
,z
。檢查y,z類型,確定操作的行爲。
unbox y,z
。執行操作符的行爲。
box x
。把
x
寫入內存。只有第
5
步驟是真正有效的操作,其他步驟都是爲第5
步驟做準備/收尾,JAVASCRIPT
的untyped
特性很好用,但也爲此付出了很大的性能代價。
JIT
。
先看看
JIT
對untyped
的優化,在JIT
下,執行x=y+z
流程。
從內存,讀取
x=y+z
的操作符。從內存,讀取
y
,z
。檢查
y
,z
類型,確定操作的行爲。
unbox y,z
。執行 操作符 的行爲。
box x
。把
x
寫入內存。其中
1
,2
步驟由CPU
負責,7
步驟JIT
把結果保存在寄存器裏。但可惜不是所有情況都能使用JIT,當number+number
,string+string
等等可以使用JIT
,但特殊情況,如:number+undefined
就不行了,只能走舊解析器。新引擎還對“對象屬性”訪問做了優化,解決方案叫
inline caching
,簡稱:IC
。簡單的說,就是做cache
。但如果當list
很大時,這種方案反而影響效率。
Type-specializing JIT
Type-specializing JIT
引擎用來處理typed
類型(聲明類型)變量,但JAVASCRIPT
都是untype
類型的。
Type-specializing JIT
的解決方案是:
先通過掃描,監測類型。
通過編譯優化(優化對象不僅僅只是“類型”,還包括對JS代碼的優化,但核心是類型優化),生成類型變量。
再做後續計算。
Type-specializing JIT
的執行x=y+z
流程:
從內存,讀取
x=y+z
的操作符。從內存,讀取
y
,z
。檢查
y
,z
類型,確定操作的行爲。
unbox y,z
。執行操作符的行爲。
box x
。把
x
寫入內存。代價是:
前置的掃描類型
編譯優化。
所以·Type-specializing JIT·的應用是有選擇性,選擇使用這個引擎的場景包括:
熱點代碼。
通過啓發式算法估算出來的有價值的代碼。
另外,有2點也需要注意:
當變量類型 發生變化時,引擎有2種處理方式:
少量變更,重編譯,再執行。
大量變更,交給JIT執行。
數組
,object properties
, 閉包變量 不在優化範疇之列。
js載入優化
加快JavaScript裝入速度的工具:
Lab.js
藉助LAB.js(裝入和阻止JavaScript),你就可以並行裝入JavaScript文件,加快總的裝入過程。此外,你還可以爲需要裝入的腳本設置某個順序,那樣就能確保依賴關係的完整性。此外,開發者聲稱其網站上的速度提升了2倍。
使用適當的CDN:
現在許多網頁使用內容分發網絡(CDN)。它可以改進你的緩存機制,因爲每個人都可以使用它。它還能爲你節省一些帶寬。你很容易使用ping檢測或使用Firebug調試那些服務器,以便搞清可以從哪些方面加快數據的速度。選擇CDN時,要照顧到你網站那些訪客的位置。記得儘可能使用公共存儲庫。
網頁末尾裝入JavaScript:
也可以在頭部分放置需要裝入的一些JavaScript,但是前提是它以異步方式裝入。
異步裝入跟蹤代碼:
腳本加載與解析會阻塞HTML渲染,可以通過異步加載方式來避免渲染阻塞,步加載的方式很多,比較通用的方法如下。
var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA-XXXXXXX-XX']); _gaq.push(['_trackPageview']); (function() { var ga = document.createElement('script'); ga.type = 'text/JavaScript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })();
或者
function loadjs (script_filename){ var script = document.createElement( 'script' ); script.setAttribute( 'type' , 'text/javascript' ); script.setAttribute( 'src' , script_filename); script.setAttribute( 'id' , 'script-id' ); scriptElement = document.getElementById( 'script-id' ); if (scriptElement){ document.getElementsByTagName( 'head' )[0].removeChild(scriptElement); } document.getElementsByTagName( 'head' )[0].appendChild(script); } var script = 'scripts/alert.js' ; loadjs(script);
把你的JavaScript打包成PNG文件
將JavaScript/css數據打包成PNG文件。之後進行拆包,只要使用畫布API的getImageData()。可以在不縮小數據的情況下,多壓縮35%左右。而且是無損壓縮,對比較龐大的腳本來說,在圖片指向畫布、讀取像素的過程中,你會覺得有“一段”裝入時間。
設置Cache-Control和Expires頭
通過Cache-Control和Expires頭可以將腳本文件緩存在客戶端或者代理服務器上,可以減少腳本下載的時間。
Expires格式:
Expires = "Expires" ":" HTTP-date Expires: Thu, 01 Dec 1994 16:00:00 GMT Note: if a response includes a Cache-Control field with the max-age directive that directive overrides the Expires field.
Cache-Control格式:
Cache-Control = "Cache-Control" ":" 1#cache-directive Cache-Control: public
具體的標準定義可以參考http1.1中的定義,簡單來說Expires控制過期時間是多久,Cache-Control控制什麼地方可以緩存 。
with
優化
儘可能地少用
with
語句,因爲它會增加with
語句以外的數據的訪問代價。避免使用
with
> `with`語句將一個新的可變對象推入作用域鏈的頭部,函數的所有局部變量現在處於第二個作用域鏈對象中,從而使局部變量的訪問代價提高。
var person = {
name: “Nicholas", age: 30
}
function displayInfo() {var count = 5; with (person) { alert(name + ' is ' + age); alert( 'count is ' + count); }
}
變量專題
全局變量
當一個變量被定義在全局作用域中,默認情況下
JAVASCRIPT
引擎就不會將其回收銷燬。如此該變量就會一直存在於老生代堆內存中,直到頁面被關閉。
全局變量
缺點。
使變量不易被回收。
多人協作時容易產生混淆。
在作用域鏈中容易被幹擾。
可以通過包裝函數來處理
全局變量
。局部變量。
儘量選用局部變量而不是全局變量。
局部變量的訪問速度要比全局變量的訪問速度更快,因爲全局變量其實是
window
對象的成員,而局部變量是放在函數的棧裏的。手工解除變量引用
在業務代碼中,一個變量已經確定不再需要了,那麼就可以手工解除變量引用,以使其被回收。
var data = { / some big data / };
// ...
data = null;變量查找優化。
變量聲明帶上
var
,如果聲明變量忘記了var
,那麼JAVASCRIPT
引擎將會遍歷整個作用域查找這個變量,結果不管找到與否,都會造成性能損耗。
如果在上級作用域找到了這個變量,上級作用域變量的內容將被無聲的改寫,導致莫名奇妙的錯誤發生。
如果在上級作用域沒有找到該變量,這個變量將自動被聲明爲全局變量,然而卻都找不到這個全局變量的定義。
慎用全局變量。
全局變量需要搜索更長的作用域鏈。
全局變量的生命週期比局部變量長,不利於內存釋放。
過多的全局變量容易造成混淆,增大產生bug的可能性。
具有相同作用域變量通過一個var聲明。
jQuery.extend = jQuery.fn.extend = function () { var options, name, src, copy, copyIsArray, clone,target = arguments[0] || {}, i = 1, length = arguments.length, deep = false ; }
緩存重複使用的全局變量。
全局變量要比局部變量需要搜索的作用域長
重複調用的方法也可以通過局部緩存來提速
該項優化在IE上體現比較明顯
var docElem = window.document.documentElement,
selector_hasDuplicate, matches = docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector ||docElem.msMatchesSelector, selector_sortOrder = function ( a, b ) { // Flag for duplicate removal if ( a === b ) { selector_hasDuplicate = true ; return 0; } }
善用回調。
除了使用閉包進行內部變量訪問,我們還可以使用現在十分流行的回調函數來進行業務處理。
function getData(callback) { var data = 'some big data'; callback(null, data); } getData(function(err, data) { console.log(data); });
回調函數是一種後續傳遞風格(
Continuation Passing Style
,CPS
)的技術,這種風格的程序編寫將函數的業務重點從返回值轉移到回調函數中去。而且其相比閉包的好處也有很多。
如果傳入的參數是基礎類型(如字符串、數值),回調函數中傳入的形參就會是複製值,業務代碼使用完畢以後,更容易被回收。
通過回調,我們除了可以完成同步的請求外,還可以用在異步編程中,這也就是現在非常流行的一種編寫風格。
回調函數自身通常也是臨時的匿名函數,一旦請求函數執行完畢,回調函數自身的引用就會被解除,自身也得到回收。
常規優化
傳遞方法取代方法字符串
一些方法例如
setTimeout()
、setInterval()
,接受字符串
或者方法實例
作爲參數。直接傳遞方法對象作爲參數來避免對字符串的二次解析。
傳遞方法
setTimeout(test, 1);
傳遞方法字符串
setTimeout('test()', 1);
使用原始操作代替方法調用
方法調用一般封裝了原始操作,在性能要求高的邏輯中,可以使用原始操作代替方法調用來提高性能。
原始操作
var min = a<b?a:b;
方法實例
var min = Math.min(a, b);
定時器
如果針對的是不斷運行的代碼,不應該使用
setTimeout
,而應該是用setInterval
。setTimeout
每次要重新設置一個定時器。避免雙重解釋
當
JAVASCRIPT
代碼想解析JAVASCRIPT
代碼時就會存在雙重解釋懲罰,雙重解釋一般在使用eval
函數、new Function
構造函數和setTimeout
傳一個字符串時等情況下會遇到,如。eval("alert('hello world');"); var sayHi = new Function("alert('hello world');"); setTimeout("alert('hello world');", 100);
上述
alert('hello world');
語句包含在字符串中,即在JS代碼運行的同時必須新啓運一個解析器來解析新的代碼,而實例化一個新的解析器有很大的性能損耗。我們看看下面的例子: var sum, num1 = 1, num2 = 2; /**效率低**/ for(var i = 0; i < 10000; i++){ var func = new Function("sum+=num1;num1+=num2;num2++;"); func(); //eval("sum+=num1;num1+=num2;num2++;"); } /**效率高**/ for(var i = 0; i < 10000; i++){ sum+=num1; num1+=num2; num2++; }
第一種情況我們是使用了new Function來進行雙重解釋,而第二種是避免了雙重解釋。
原生方法更快
只要有可能,使用原生方法而不是自已用JS重寫。原生方法是用諸如C/C++之類的編譯型語言寫出來的,要比JS的快多了。
最小化語句數
JS代碼中的語句數量也會影響所執行的操作的速度,完成多個操作的單個語句要比完成單個操作的多個語句塊快。故要找出可以組合在一起的語句,以減來整體的執行時間。這裏列舉幾種模式
多個變量聲明
/不提倡/
var i = 1;
var j = "hello";
var arr = [1,2,3];
var now = new Date();
/提倡/
var i = 1,j = "hello", arr = [1,2,3], now = new Date();
插入迭代值
/不提倡/
var name = values[i];
i++;
/提倡/
var name = values[i++];使用數組和對象字面量,避免使用構造函數Array(),Object()
/不提倡/
var a = new Array();
a[0] = 1;
a[1] = "hello";
a[2] = 45;
var o = new Obejct();
o.name = "bill";
o.age = 13;
/提倡/
var a = [1, "hello", 45];
var o = {name : "bill", age : 13
};
避免使用屬性訪問方法
JavaScript不需要屬性訪問方法,因爲所有的屬性都是外部可見的。
添加屬性訪問方法只是增加了一層重定向 ,對於訪問控制沒有意義。
使用屬性訪問方法示例
function Car() {
this .m_tireSize = 17;
this .m_maxSpeed = 250;
this .GetTireSize = Car_get_tireSize;
this .SetTireSize = Car_put_tireSize;
}function Car_get_tireSize() {
return this .m_tireSize;
}function Car_put_tireSize(value) {
this .m_tireSize = value;
}
var ooCar = new Car();
var iTireSize = ooCar.GetTireSize();
ooCar.SetTireSize(iTireSize + 1);直接訪問屬性示例
function Car() {
this .m_tireSize = 17;
this .m_maxSpeed = 250;
}
var perfCar = new Car();
var iTireSize = perfCar.m_tireSize;
perfCar.m_tireSize = iTireSize + 1;減少使用元素位置操作
一般瀏覽器都會使用增量reflow的方式將需要reflow的操作積累到一定程度然後再一起觸發,但是如果腳本中要獲取以下屬性,那麼積累的reflow將會馬上執行,已得到準確的位置信息。
offsetLeft offsetTop offsetHeight offsetWidth scrollTop/Left/Width/Height clientTop/Left/Width/Height getComputedStyle()
代碼壓縮
代碼壓縮工具
精簡代碼就是將代碼中的
空格
和註釋
去除,也有更進一步的會對變量名稱混淆
、精簡
。根據統計精簡後文件大小會平均減少21%
,即使Gzip
之後文件也會減少5%
。
YUICompressor
Dean Edwards Packer
JSMin
GZip壓縮
GZip
縮短在瀏覽器和服務器之間傳送數據的時間,縮短時間後得到標題是Accept-Encoding
:gzip
,deflate
的一個文件。不過這種壓縮方法同樣也有缺點。
它在服務器端和客戶端都要佔用處理器資源(以便壓縮和解壓縮)。
佔用磁盤空間。
Gzip
通常可以減少70%網頁內容的大小,包括腳本、樣式表、圖片等任何一個文本類型的響應,包括XML
和JSON
。Gzip
比deflate
更高效,主流服務器都有相應的壓縮支持模塊。
Gzip
的工作流程爲
客戶端在請求
Accept-Encoding
中聲明可以支持Gzip
。服務器將請求文檔壓縮,並在
Content-Encoding
中聲明該回復爲Gzip
格式。客戶端收到之後按照
Gzip
解壓縮。Closure compiler
代碼優化
優化原則:
JS與其他語言不同在於它的執行效率很大程度是取決於
JS engine
的效率。除了引擎實現
的優劣外,引擎
自己也會爲一些特殊的代碼模式
採取一些優化的策略。例如FF
、Opera
和Safari
的JAVASCRIPT
引擎,都對字符串的拼接運算(+
)做了特別優化。所以應該根據不同引擎進行不同優化。而如果做跨瀏覽器的web編程,則最大的問題是在於IE6(JScript 5.6),因爲在不打hotfix的情況下,JScript引擎的垃圾回收的bug,會導致其在真實應用中的performance跟其他瀏覽器根本不在一個數量級上。因此在這種場合做優化,實際上就是爲JScript做優化,所以第一原則就是只需要爲IE6(未打補丁的JScript 5.6或更早版本)做優化。
JS優化總是出現在大規模循環的地方:
這倒不是說循環本身有性能問題,而是循環會迅速放大可能存在的性能問題,所以第二原則就是以大規模循環體爲最主要優化對象。
以下的優化原則,只在大規模循環中才有意義,在循環體之外做此類優化基本上是沒有意義的。
目前絕大多數JS引擎都是解釋執行的,而解釋執行的情況下,在所有操作中,函數調用的效率是較低的。此外,過深的prototype繼承鏈或者多級引用也會降低效率。JScript中,10級引用的開銷大體是一次空函數調用開銷的1/2。這兩者的開銷都遠遠大於簡單操作(如四則運算)。
儘量避免過多的引用層級和不必要的多次方法調用:
特別要注意的是,有些情況下看似是屬性訪問,實際上是方法調用。例如所有DOM的屬性,實際上都是方法。在遍歷一個NodeList的時候,循環 條件對於nodes.length的訪問,看似屬性讀取,實際上是等價於函數調用的。而且IE DOM的實現上,childNodes.length每次是要通過內部遍歷重新計數的。(My god,但是這是真的!因爲我測過,childNodes.length的訪問時間與childNodes.length的值成正比!)這非常耗費。所以 預先把nodes.length保存到js變量,當然可以提高遍歷的性能。
同樣是函數調用,用戶自定義函數的效率又遠遠低於語言內建函數,因爲後者是對引擎本地方法的包裝,而引擎通常是c,c++,java寫的。進一步,同樣的功能,語言內建構造的開銷通常又比內建函數調用要效率高,因爲前者在JS代碼的parse階段就可以確定和優化。
儘量使用語言本身的構造和內建函數:
這裏有一個例子是高性能的String.format方法。 String.format傳統的實現方式是用String.replace(regex, func),在pattern包含n個佔位符(包括重複的)時,自定義函數func就被調用n次。而這個高性能實現中,每次format調用所作的只是一次Array.join然後一次String.replace(regex, string)的操作,兩者都是引擎內建方法,而不會有任何自定義函數調用。兩次內建方法調用和n次的自定義方法調用,這就是性能上的差別。
同樣是內建特性,性能上也還是有差別的。例如在JScript中對於arguments的訪問性能就很差,幾乎趕上一次函數調用了。因此如果一個 可變參數的簡單函數成爲性能瓶頸的時候,可以將其內部做一些改變,不要訪問arguments,而是通過對參數的顯式判斷來處理,比如:
動畫優化
動畫效果在缺少硬件加速支持的情況下反應緩慢,例如手機客戶端。
特效應該只在確實能改善用戶體驗時才使用,而不應用於炫耀或者彌補功能與可用性上的缺陷。
至少要給用戶一個選擇可以禁用動畫效果。
設置動畫元素爲absolute或fixed。
position: static
或position: relative
元素應用動畫效果會造成頻繁的reflow
。
position: absolute
或position: fixed
的元素應用動畫效果只需要repaint
。使用一個
timer
完成多個元素動畫。
setInterval
和setTimeout
是兩個常用的實現動畫的接口,用以間隔更新元素的風格與佈局。。動畫效果的幀率最優化的情況是使用一個
timer
完成多個對象的動畫效果,其原因在於多個timer
的調用本身就會損耗一定性能。setInterval(function() { animateFirst(''); }, 10); setInterval(function() { animateSecond(''); }, 10);
使用同一個
timer
。setInterval(function() { animateFirst(''); animateSecond(''); }, 10);
以腳本爲基礎的動畫,由瀏覽器控制動畫的更新頻率。
對象專題
減少不必要的對象創建:
創建對象本身對性能影響並不大,但由於
JAVASCRIPT
的垃圾回收調度算法,導致隨着對象個數的增加,性能會開始嚴重下降(複雜度O(n^2)
)。
如常見的字符串拼接問題,單純的多次創建字符串對象其實根本不是降低性能的主要原因,而是是在對象創建期間的無謂的垃圾回收的開銷。而
Array.join
的方式,不會創建中間字符串對象,因此就減少了垃圾回收的開銷。複雜的
JAVASCRIPT
對象,其創建時時間和空間的開銷都很大,應該儘量考慮採用緩存。儘量作用
JSON
格式來創建對象,而不是var obj=new Object()
方法。前者是直接複製,而後者需要調用構造器。對象查找
避免對象的嵌套查詢,因爲
JAVASCRIPT
的解釋性,a.b.c.d.e
嵌套對象,需要進行4
次查詢,嵌套的對象成員會明顯影響性能。如果出現嵌套對象,可以利用局部變量,把它放入一個臨時的地方進行查詢。
對象屬性
訪問對象屬性消耗性能過程(
JAVASCRIPT
對象存儲)。
先從本地變量表找到
對象
。然後遍歷
屬性
。如果在
當前對象
的屬性列表
裏沒找到。繼續從
prototype
向上查找。且不能直接索引,只能遍歷。
function f(obj) {
return obj.a + 1;
}
服務端優化
避免404。
更改404錯誤響應頁面可以改進用戶體驗,但是同樣也會浪費服務器資源。
指向外部
JAVASCRIPT
的鏈接出現問題並返回404代碼。
這種加載會破壞並行加載。
其次瀏覽器會把試圖在返回的404響應內容中找到可能有用的部分當作JavaScript代碼來執行。
刪除重複的
JAVASCRIPT
和CSS
。
重複調用腳本缺點。
增加額外的HTTP請求。
多次運算也會浪費時間。在IE和Firefox中不管腳本是否可緩存,它們都存在重複運算
JAVASCRIPT
的問題。
ETags
配置Entity
標籤。
ETags
用來判斷瀏覽器緩存裏的元素是否和原來服務器上的一致。
與
last-modified date
相比更靈活。>如某個文件在1秒內修改了10次,`ETags`可以綜合`Inode`(文件的索引節點`inode`數),`MTime`(修改時間)和`Size`來精準的進行判斷,避開`UNIX`記錄`MTime`只能精確到秒的問題。服務器集羣使用,可取後兩個參數。使用`ETags`減少`Web`應用帶寬和負載
權衡DNS查找次數
減少主機名可以節省響應時間。但同時也會減少頁面中並行下載的數量。
IE
瀏覽器在同一時刻只能從同一域名下載兩個文件。當在一個頁面顯示多張圖片時,IE
用戶的圖片下載速度就會受到影響。通過Keep-alive機制減少TCP連接。
通過CDN減少延時。
平行處理請求(參考BigPipe)。
通過合併文件或者Image Sprites減少HTTP請求。
減少重定向( HTTP 301和40x/50x)。
類型轉換專題
把數字轉換成字符串。
應用
""+1
,效率是最高。
性能上來說:
""+字符串
>String()
>.toString()
>new String()
。
String()
屬於內部函數,所以速度很快。
.toString()
要查詢原型中的函數,所以速度略慢。
new String()
最慢。浮點數轉換成整型。
錯誤使用使用
parseInt()
。
parseInt()
是用於將字符串
轉換成數字
,而不是浮點數
和整型
之間的轉換。應該使用
Math.floor()
或者Math.round()
。
Math
是內部對象,所以Math.floor()
其實並沒有多少查詢方法和調用的時間,速度是最快的。
邏輯判斷優化
switch
語句。
若有一系列複雜的
if-else
語句,可以轉換成單個switch
語句則可以得到更快的代碼,還可以通過將case
語句按照最可能的到最不可能的順序進行組織,來進一步優化。
內存專題
JAVASCRIPT
的內存回收機制
以Google的
V8
引擎爲例,在V8
引擎中所有的JAVASCRIPT
對象都是通過堆
來進行內存分配的。當我們在代碼中聲明變量
並賦值
時,V8
引擎就會在堆內存
中分配一部分給這個變量
。如果已申請的內存
不足以存儲這個變量
時,V8
引擎就會繼續申請內存
,直到堆
的大小達到了V8
引擎的內存上限爲止(默認情況下,V8
引擎的堆內存
的大小上限在64位系統
中爲1464MB
,在32位系統
中則爲732MB
)。另外,
V8
引擎對堆內存
中的JAVASCRIPT
對象進行分代管理
。
新生代。
新生代即存活週期較短的
JAVASCRIPT
對象,如臨時變量、字符串等老生代。
老生代則爲經過多次垃圾回收仍然存活,存活週期較長的對象,如主控制器、服務器對象等。
垃圾回收算法。
垃圾回收算法一直是編程語言的研發中是否重要的一環,而
V8
引擎所使用的垃圾回收算法主要有以下幾種。
Scavange
算法:通過複製的方式進行內存空間管理,主要用於新生代的內存空間;
Mark-Sweep
算法和Mark-Compact
算法:通過標記來對堆內存進行整理和回收,主要用於老生代對象的檢查和回收。對象進行回收。
引用
。
當函數執行完畢時,在函數內部所聲明的對象
不一定
就會被銷燬。引用(
Reference
)是JAVASCRIPT
編程中十分重要的一個機制。
是指
代碼對對象的訪問
這一抽象關係,它與C/C++
的指針有點相似,但並非同物。引用同時也是JAVASCRIPT
引擎在進行垃圾回收
中最關鍵的一個機制。var val = 'hello world';
function foo() {
return function() {return val;
};
}
global.bar = foo();
當代碼執行完畢時,對象
val
和bar()
並沒有被回收釋放,JAVASCRIPT
代碼中,每個變量
作爲單獨一行而不做任何操作,JAVASCRIPT
引擎都會認爲這是對對象
的訪問行爲,存在了對對象的引用
。爲了保證垃圾回收
的行爲不影響程序邏輯的運行,JAVASCRIPT
引擎不會把正在使用的對象
進行回收。所以判斷對象
是否正在使用中的標準,就是是否仍然存在對該對象
的引用
。
JAVASCRIPT
的引用
是可以進行轉移
的,那麼就有可能出現某些引用被帶到了全局作用域,但事實上在業務邏輯裏已經不需要對其進行訪問了,這個時候就應該被回收,但是JAVASCRIPT
引擎仍會認爲程序仍然需要它。
IE
下閉包引起跨頁面內存泄露。
JAVASCRIPT
的內存泄露處理
給
DOM
對象添加的屬性是一個對象的引用。var MyObject = {};
document.getElementByIdx_x('myDiv').myProp = MyObject;解決方法:在window.onunload事件中寫上:
document.getElementByIdx_x('myDiv').myProp = null;
DOM對象與JS對象相互引用。
function Encapsulator(element) {
this.elementReference = element; element.myProp = this;
}
new Encapsulator(document.getElementByIdx_x('myDiv'));解決方法:在onunload事件中寫上:
document.getElementByIdx_x('myDiv').myProp = null;
給DOM對象用attachEvent綁定事件。
function doClick() {}
element.attachEvent("onclick", doClick);解決方法:在onunload事件中寫上:
element.detachEvent('onclick', doClick);
從外到內執行appendChild。這時即使調用removeChild也無法釋放。
var parentDiv = document.createElement_x("div");
var childDiv = document.createElement_x("div");
document.body.appendChild(parentDiv);
parentDiv.appendChild(childDiv);解決方法:從內到外執行appendChild:
var parentDiv = document.createElement_x("div");
var childDiv = document.createElement_x("div");
parentDiv.appendChild(childDiv);
document.body.appendChild(parentDiv);反覆重寫同一個屬性會造成內存大量佔用(但關閉IE後內存會被釋放)。
for(i = 0; i < 5000; i++) {
hostElement.text = "asdfasdfasdf";
}
這種方式相當於定義了5000個屬性,解決方法:無。
內存
不是緩存
。
不要輕易將
內存
當作緩存
使用。如果是很重要的資源,請不要直接放在
內存
中,或者制定過期機制
,自動銷燬過期緩存
。
CollectGarbage
。
CollectGarbage
是IE
的一個特有屬性,用於釋放內存的使用方法,將該變量或引用對象設置爲null
或delete
然後在進行釋放動作,在做CollectGarbage
前,要必需清楚的兩個必備條件:(引用)。
一個對象在其生存的上下文環境之外,即會失效。
一個全局的對象在沒有被執用(引用)的情況下,即會失效
事件優化
使用事件代理
當存在多個元素需要註冊事件時,在每個元素上綁定事件本身就會對性能有一定損耗。
由於DOM Level2事件模 型中所有事件默認會傳播到上層文檔對象,可以藉助這個機制在上層元素註冊一個統一事件對不同子元素進行相應處理。
捕獲型事件先發生。兩種事件流會觸發DOM中的所有對象,從document對象開始,也在document對象結束。
<ul id="parent-list"> <li id="post-1">Item 1 <li id="post-2">Item 2 <li id="post-3">Item 3 <li id="post-4">Item 4 <li id="post-5">Item 5 <li id="post-6">Item 6 </li></ul> // Get the element, add a click listener... document.getElementById("parent-list").addEventListener("click",function(e) { // e.target is the clicked element! // If it was a list item if(e.target && e.target.nodeName == "LI") { // List item found! Output the ID! console.log("List item ",e.target.id.replace("post-")," was clicked!"); } });
數組專題
當需要使用數組時,可使用
JSON
格式的語法
即直接使用如下語法定義數組:
[parrm,param,param...]
,而不是採用new Array(parrm,param,param...)
這種語法。使用JSON
格式的語法是引擎直接解釋。而後者則需要調用Array
的構造器。如果需要遍歷數組,應該先緩存數組長度,將數組長度放入局部變量中,避免多次查詢數組長度。
根據字符串、數組的長度進行循環,而通常這個長度是不變的,比如每次查詢
a.length
,就要額外進行一個操作,而預先把var len=a.length
,則每次循環就少了一次查詢。
同域跨域
避免跳轉
同域:注意避免反斜槓 “/” 的跳轉;
跨域:使用Alias或者mod_rewirte建立CNAME(保存域名與域名之間關係的DNS記錄)
性能測試工具
js性能優化和內存泄露問題及檢測分析工具
性能優化ajax工具
diviefirebug
[web性能分析工具YSlow]
performance
性能評估打分,右擊箭頭可看到改進建議。
stats
緩存狀態分析,傳輸內容分析。
components
所有加載內容分析,可以查看傳輸速度,找出頁面訪問慢的瓶頸。
tools
可以查看js和css,並打印頁面評估報告。內存泄露檢測工具
sIEve
sIEve
是基於IE
的內存泄露檢測工具,需要下載運行,可以查看dom孤立節點和內存泄露及內存使用情況。
列出當前頁面內所有dom節點的基本信息(html id style 等)
頁面內所有dom節點的高級信息 (內存佔用,數量,節點的引用)
可以查找出頁面中的孤立節點
可以查找出頁面中的循環引用
可以查找出頁面中產生內存泄露的節點
內存泄露提示工具
leak monitor
leak monitor
在安裝後,當離開一個頁面時,比如關閉窗口,如果頁面有內存泄露,會彈出一個文本框進行即時提示。代碼壓縮工具
YUI壓縮工具
Dean Edwards Packer
JSMin
Blink/Webkit
瀏覽器
在
Blink/Webkit
瀏覽器中(Chrome
,Safari
,Opera
),我們可以藉助其中的Developer Tools
的Profiles
工具來對我們的程序進行內存檢查。Developer Tools - Profiles
Node.js
中的內存檢查
在
Node.js
中,我們可以使用node-heapdump
和node-memwatch
模塊進行內存檢查。var heapdump = require('heapdump');
var fs = require('fs');
var path = require('path');
fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);在業務代碼中引入
node-heapdump
之後,我們需要在某個運行時期,向Node.js
進程發送SIGUSR2
信號,讓node-heapdump
抓拍一份堆內存的快照。$ kill -USR2 (cat app.pid) 這樣在文件目錄下會有一個以`heapdump-<sec>.<usec>.heapsnapshot`格式命名的快照文件,我們可以使用瀏覽器的`Developer Tools`中的`Profiles`工具將其打開,並進行檢查。
分析瀏覽器提供的Waterfall圖片來思考優化入口。
新的測試手段(Navigation, Resource, 和User timing。
循環專題
循環是一種常用的流程控制。
JAVASCRIPT
提供了三種循環。
for(;;)
。
推薦使用for循環,如果循環變量遞增或遞減,不要單獨對循環變量賦值,而應該使用嵌套的
++
或–-
運算符。代碼的可讀性對於for循環的優化。
用
-=1
。從大到小的方式循環(這樣缺點是降低代碼的可讀性)。
/效率低/
var divs = document.getElementsByTagName("div");
for(var i = 0; i < divs.length; i++){...
}
/效率高,適用於獲取DOM集合,如果純數組則兩種情況區別不到/
var divs = document.getElementsByTagName("div");
for(var i = 0, len = divs.length; i < len; i++){...
}
/在IE6.0
下,for(;;)
循環在執行中,第一種情況會每次都計算一下長度,而第二種情況卻是在開始的時候計算長度,並把其保存到一個變量中,所以其執行效率要高點,所以在我們使用for(;;)
循環的時候,特別是需要計算長度的情況,我們應該開始將其保存到一個變量中。/
while()
。
for(;;)
、while()
循環的性能基本持平。
for(in)
。
在這三種循環中
for(in)
內部實現是構造一個所有元素的列表,包括array
繼承的屬性,然後再開始循環,並且需要查詢hasOwnProperty。所以for(in)
相對for(;;)
循環性能要慢。選擇正確的方法
避免不必要的屬性查找。
訪問
變量
或數組
是O(1)
操作。訪問
對象
上的屬性
是一個O(n)
操作。對象上的任何屬性查找都要比訪問變量或數組花費更長時間,因爲必須在原型鏈中對擁有該名稱的屬性進行一次搜索,即屬性查找越多,執行時間越長。所以針對需要多次用到對象屬性,應將其存儲在局部變量。
優化循環。
減值迭代。
大多數循環使用一個從0開始,增加到某個特定值的迭代器。在很多情況下,從最大值開始,在循環中不斷減值的迭代器更加有效。
簡化終止條件。
由於每次循環過程都會計算終止條件,故必須保證它儘可能快,即避免屬性查找或其它O(n)的操作。
簡化循環體。
循環體是執行最多的,故要確保其被最大限度地優化。確保沒有某些可以被很容易移出循環的密集計算。
使用後測試循環。
最常用的for和while循環都是前測試循環,而如do-while循環可以避免最初終止條件的計算,因些計算更快。
for(var i = 0; i < values.length; i++) { process(values[i]); }
優化1:簡化終止條件
for(var i = 0, len = values.length; i < len; i++) { process(values[i]); }
優化2:使用後測試循環(注意:使用後測試循環需要確保要處理的值至少有一個)
展開循環。
當循環的次數確定時,消除循環並使用多次函數調用往往更快。
當循環的次數不確定時,可以使用Duff裝置來優化。
Duff裝置的基本概念是通過計算迭代的次數是否爲8的倍數將一個循環展開爲一系列語句。
// Jeff Greenberg for JS implementation of Duff's Device // 假設:values.length 0 function process(v) { alert(v); } var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]; var iterations = Math.ceil(values.length / 8); var startAt = values.length % 8; var i = 0; do { switch(startAt) { case 0 : process(values[i++]); case 7 : process(values[i++]); case 6 : process(values[i++]); case 5 : process(values[i++]); case 4 : process(values[i++]); case 3 : process(values[i++]); case 2 : process(values[i++]); case 1 : process(values[i++]); } startAt = 0; }while(--iterations 0);
如上展開循環可以提升大數據集的處理速度。接下來給出更快的Duff裝置技術,將do-while循環分成2個單獨的循環。(注:這種方法幾乎比原始的Duff裝置實現快上40%。)
// Speed Up Your Site(New Riders, 2003) function process(v) { alert(v); } var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]; var iterations = Math.floor(values.length / 8); var leftover = values.length % 8; var i = 0; if(leftover 0) { do { process(values[i++]); }while(--leftover 0); } do { process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); }while(--iterations 0);
針對大數據集使用展開循環可以節省很多時間,但對於小數據集,額外的開銷則可能得不償失。
避免在循環中使用
try-catch
。
try-catch-finally
語句在catch語句被執行的過程中會動態構造變量插入到當前域中,對性能有一定影響。如果需要異常處理機制,可以將其放在循環外層使用。
循環中使用try-catch
for ( var i = 0; i < 200; i++) {
try {} catch (e) {}
}
循環外使用try-catch
try { for ( var i = 0; i < 200; i++) {} } catch (e) {}
避免遍歷大量元素:
避免對全局
DOM
元素進行遍歷,如果parent
已知可以指定parent
在特定範圍查詢。var elements = document.getElementsByTagName( '*' );
for (i = 0; i < elements.length; i++) {
if (elements[i].hasAttribute( 'selected' )) {}
}如果已知元素存在於一個較小的範圍內,
var elements = document.getElementById( 'canvas' ).getElementsByTagName ( '*' );
for (i = 0; i < elements.length; i++) {
if (elements[i].hasAttribute( 'selected' )) {}
}
原型優化
通過原型優化方法定義。
如果一個方法類型將被頻繁構造,通過方法原型從外面定義附加方法,從而避免方法的重複定義。
可以通過外部原型的構造方式初始化值類型的變量定義。(這裏強調值類型的原因是,引用類型如果在原型中定義,一個實例對引用類型的更改會影響到其他實例。)
這條規則中涉及到
JAVASCRIPT
中原型的概念,構造函數都有一個prototype
屬性,指向另一個對象。這個對象的所有屬性和方法,都會被構造函數的實例繼承。可以把那些不變的屬性和方法,直接定義在prototype
對象上。
可以通過對象實例訪問保存在原型中的值。
不能通過對象實例重寫原型中的值。
在實例中添加一個與實例原型同名屬性,那該屬性就會屏蔽原型中的屬性。
通過delete操作符可以刪除實例中的屬性。
運算符專題
使用運算符時,儘量使用
+=
,-=
、*=
、\=
等運算符號,而不是直接進行賦值運算。
位運算
。
當進行數學運算時
位運算
較快,位運算
操作要比任何布爾運算
或算數運算
快,如取模
,邏輯與
和邏輯或
也可以考慮用位運算
來替換。
重繪專題
減少頁面的
重繪
。
減少頁面
重繪
雖然本質不是JAVASCRIPT
優化,但重繪
往往是由JAVASCRIPT
引起的,而重繪
的情況直接影響頁面性能。var str = "<div>這是一個測試字符串</div>";
/效率低/
var obj = document.getElementsByTagName("body");
for(var i = 0; i < 100; i++){obj.innerHTML += str + i;
}
/效率高/
var obj = document.getElementsByTagName("body");
var arr = [];
for(var i = 0; i < 100; i++){arr[i] = str + i;
}
obj.innerHTML = arr.join("");一般影響頁面重繪的不僅僅是innerHTML,如果改變元素的樣式,位置等情況都會觸發頁面重繪,所以在平時一定要注意這點。
使用HTML5和CSS3的一些新特性。
避免在HTML裏面縮放圖片。
避免使用插件。
確保使用正確的字體大小。
決定當前頁面是不是能被訪問。
字符串專題
對字符串進行循環操作。
替換、查找等操作,使用正則表達式。
因爲
JAVASCRIPT
的循環速度較慢,而正則表達式的操作是用C
寫成的API
,性能比較好。字符串的拼接。
字符串的拼接在我們開發中會經常遇到,所以我把其放在首位,我們往往習慣的直接用
+=
的方式來拼接字符串,其實這種拼接的方式效率非常的低,我們可以用一種巧妙的方法來實現字符串的拼接,那就是利用數組的join
方法,具體請看我整理的:Web前端開發規範文檔中的javaScript書寫規範
倒數第三條目。不過也有另一種說法,通常認爲需要用
Array.join
的方式,但是由於SpiderMonkey
等引擎對字符串的“+
”運算做了優化,結果使用Array.join
的效率反而不如直接用“+
”,但是如果考慮IE6
,則其他瀏覽器上的這種效率的差別根本不值一提。具體怎麼取捨,諸君自定。
作用域鏈和閉包優化
作用域。
作用域(
scope
)是JAVASCRIPT
編程中一個重要的運行機制
,在JAVASCRIPT
同步和異步編程以及JAVASCRIPT
內存管理中起着至關重要的作用。在
JAVASCRIPT
中,能形成作用域的有如下幾點。
函數的調用
with語句
with
會創建自已的作用域,因此會增加其中執行代碼的作用域的長度。全局作用域。
以下代碼爲例:
var foo = function() {
var local = {};
};
foo();
console.log(local); //=undefinedvar bar = function() {
local = {};
};
bar();
console.log(local); //={}/這裏我們定義了foo()函數和bar()函數,他們的意圖都是爲了定義一個名爲local的變量。在foo()函數中,我們使用var語句來聲明定義了一個local變量,而因爲函數體內部會形成一個作用域,所以這個變量便被定義到該作用域中。而且foo()函數體內並沒有做任何作用域延伸的處理,所以在該函數執行完畢後,這個local變量也隨之被銷燬。而在外層作用域中則無法訪問到該變量。而在bar()函數內,local變量並沒有使用var語句進行聲明,取而代之的是直接把local作爲全局變量來定義。故外層作用域可以訪問到這個變量。/
local = {}; // 這裏的定義等效於 global.local = {};
作用域鏈
在
JAVASCRIPT
編程中,會遇到多層函數嵌套的場景,這就是典型的作用域鏈的表示。function foo() {
var val = 'hello';
function bar() {function baz() { global.val = 'world;' }; baz(); console.log(val); //=hello
};
bar();
};
foo();/**在`JAVASCRIPT`中,變量標識符的查找是從當前作用域開始向外查找,直到全局作用域爲止。所以`JAVASCRIPT`代碼中對變量的訪問只能向外進行,而不能逆而行之。baz()函數的執行在全局作用域中定義了一個全局變量val。而在bar()函數中,對val這一標識符進行訪問時,按照從內到外的查找原則:在bar函數的作用域中沒有找到,便到上一層,即foo()函數的作用域中查找。然而,使大家產生疑惑的關鍵就在這裏:本次標識符訪問在foo()函數的作用域中找到了符合的變量,便不會繼續向外查找,故在baz()函數中定義的全局變量val並沒有在本次變量訪問中產生影響。**/
減少作用域鏈上的查找次數
JAVASCRIPT
代碼在執行的時候,如果需要訪問一個變量或者一個函數的時候,它需要遍歷當前執行環境的作用域鏈,而遍歷是從這個作用域鏈的前端一級一級的向後遍歷,直到全局執行環境。/效率低/
for(var i = 0; i < 10000; i++){var but1 = document.getElementById("but1");
}
/效率高/
/避免全局查找/
var doc = document;
for(var i = 0; i < 10000; i++){var but1 = doc.getElementById("but1");
}
/上面代碼中,第二種情況是先把全局對象的變量放到函數裏面先保存下來,然後直接訪問這個變量,而第一種情況是每次都遍歷作用域鏈,直到全局環境,我們看到第二種情況實際上只遍歷了一次,而第一種情況卻是每次都遍歷了,而且這種差別在多級作用域鏈和多個全局變量的情況下還會表現的非常明顯。在作用域鏈查找的次數是O(n)
。通過創建一個指向document
的局部變量,就可以通過限制一次全局查找來改進這個函數的性能。/閉包
JAVASCRIPT
中的標識符查找遵循從內到外的原則。function foo() { var local = 'Hello'; return function() { return local; }; } var bar = foo(); console.log(bar()); //=Hello /**這裏所展示的讓外層作用域訪問內層作用域的技術便是閉包(Closure)。得益於高階函數的應用,使foo()函數的作用域得到`延伸`。foo()函數返回了一個匿名函數,該函數存在於foo()函數的作用域內,所以可以訪問到foo()函數作用域內的local變量,並保存其引用。而因這個函數直接返回了local變量,所以在外層作用域中便可直接執行bar()函數以獲得local變量。**/
閉包是
JAVASCRIPT
的高級特性,因爲把帶有內部變量引用的函數帶出了函數外部,所以該作用域內的變量在函數執行完畢後的並不一定會被銷燬,直到內部變量的引用被全部解除。所以閉包的應用很容易造成內存無法釋放的情況。良好的閉包管理。
循環事件綁定、私有屬性、含參回調等一定要使用閉包時,並謹慎對待其中的細節。
循環綁定事件,我們假設一個場景:有六個按鈕,分別對應六種事件,當用戶點擊按鈕時,在指定的地方輸出相應的事件。
var btns = document.querySelectorAll('.btn'); // 6 elements
var output = document.querySelector('#output');
var events = [1, 2, 3, 4, 5, 6];
// Case 1
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function(evt) {output.innerText += 'Clicked ' + events[i];
};
}
/這裏第一個解決方案顯然是典型的循環綁定事件錯誤,這裏不細說,詳細可以參照我給一個網友的回答;而第二和第三個方案的區別就在於閉包傳入的參數。/
// Case 2
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = (function(index) {return function(evt) { output.innerText += 'Clicked ' + events[index]; };
})(i);
}
/第二個方案傳入的參數是當前循環下標,而後者是直接傳入相應的事件對象。事實上,後者更適合在大量數據應用的時候,因爲在JavaScript的函數式編程中,函數調用時傳入的參數是基本類型對象,那麼在函數體內得到的形參會是一個複製值,這樣這個值就被當作一個局部變量定義在函數體的作用域內,在完成事件綁定之後就可以對events變量進行手工解除引用,以減輕外層作用域中的內存佔用了。而且當某個元素被刪除時,相應的事件監聽函數、事件對象、閉包函數也隨之被銷燬回收。/
// Case 3
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = (function(event) {return function(evt) { output.innerText += 'Clicked ' + event; };
})(events[i]);
}避開閉包陷阱
閉包是個強大的工具,但同時也是性能問題的主要誘因之一。不合理的使用閉包會導致內存泄漏。
閉包的性能不如使用內部方法,更不如重用外部方法。
由於
IE 9
瀏覽器的DOM
節點作爲COM
對象來實現,COM
的內存管理
是通過引用計數的方式,引用計數有個難題就是循環引用,一旦DOM
引用了閉包(例如event handler
),閉包的上層元素又引用了這個DOM
,就會造成循環引用從而導致內存泄漏。善用函數
使用一個匿名函數在代碼的最外層進行包裹。
;(function() { // 主業務代碼 })();
有的甚至更高級一點:
;(function(win, doc, $, undefined) { // 主業務代碼 })(window, document, jQuery);
甚至連如RequireJS, SeaJS, OzJS 等前端模塊化加載解決方案,都是採用類似的形式:
/**RequireJS**/ define(['jquery'], function($) { // 主業務代碼 }); /**SeaJS**/ define('module', ['dep', 'underscore'], function($, _) { // 主業務代碼 });
被定義在全局作用域的對象,可能是會一直存活到進程退出的,如果是一個很大的對象,那就麻煩了。比如有的人喜歡在JavaScript中做模版渲染:
<?php $db = mysqli_connect(server, user, password, 'myapp'); $topics = mysqli_query($db, "SELECT * FROM topics;"); ?> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>你是猴子請來的逗比麼?</title> </head> <body> <ul id="topics"></ul> <script type="text/tmpl" id="topic-tmpl"> <li class="topic"> <h1><%=title%></h1> <p><%=content%></p> </li> </script> <script type="text/javascript"> var data = <?php echo json_encode($topics); ?>; var topicTmpl = document.querySelector('#topic-tmpl').innerHTML; var render = function(tmlp, view) { var complied = tmlp .replace(/\n/g, '\\n') .replace(/<%=([\s\S]+?)%>/g, function(match, code) { return '" + escape(' + code + ') + "'; }); complied = [ 'var res = "";', 'with (view || {}) {', 'res = "' + complied + '";', '}', 'return res;' ].join('\n'); var fn = new Function('view', complied); return fn(view); }; var topics = document.querySelector('#topics'); function init() data.forEach(function(topic) { topics.innerHTML += render(topicTmpl, topic); }); } init(); </script> </body> </html>
在從數據庫中獲取到的數據的量是非常大的話,前端完成模板渲染以後,data變量便被閒置在一邊。可因爲這個變量是被定義在全局作用域中的,所以
JAVASCRIPT
引擎不會將其回收銷燬。如此該變量就會一直存在於老生代堆內存中,直到頁面被關閉。可是如果我們作出一些很簡單的修改,在邏輯代碼外包裝一層函數,這樣效果就大不同了。當UI渲染完成之後,代碼對data的引用也就隨之解除,而在最外層函數執行完畢時,JAVASCRIPT
引擎就開始對其中的對象進行檢查,data也就可以隨之被回收。