下載時間
Web瀏覽器下載的是JavaScript源碼,也就是所有的長變數與注釋都會包含在內。這個因素和其他因素都會增加下載時間,這會增加指令碼啟動並執行總時間。增加下載時間的關鍵因素就是指令碼所包含的位元組數。
要記住一個關鍵數字是1160,這是能放入單個TCP-IP包中的位元組數。最好能將每個JavaScript檔案都保持在1160位元組以下以擷取最優的下載時間。
在JavaScript中,每個字元就是一個位元組,因此,每個額外的字元(不管是變數名、函數名、或者注釋)都會影響下載速度。部署JavaScript之前,都應該儘可能最佳化下載速度。
1.刪除注釋
2.刪除定位字元和空格
3.刪除所有的換行
4.替換變數名
5.ECMAScript Cruncher
利用Thomas Loo開發的ECMAScript Cruncher(ESC 可以http://www.saltstorm.net/depo/esc/或51AJAX.com下載)。ESC是一個小巧的Window Shell指令碼。
利用Thomas Loo開發的ECMAScript Cruncher(ESC 可以下載)。ESC是一個小巧的Window Shell指令碼。運行ESC,必須使用Windows系統。開啟一個控制台視窗,輸入以下命令:
cscript ESC.wsf -l [0-4] -ow outputfile.js inputfile.js [inputfile2.js]
第一部分,cscript是Windows Shell指令碼解釋程式。檔案名稱ESC.wsf是ESC的程式本身。然後是壓縮等級,一個0到4的數值,表示要進行最佳化的等級。-ow選項表示下一個參數是最佳化後輸出的檔案名稱。最後,剩下的參數是要進行最佳化的JavaScript檔案。可以只給出一個要進行最佳化的檔案,也可以有多個檔案(多個檔案估最佳化後會按順序放到輸出檔案中)。
ESC支援的四個最佳化等級如下:
0:不改變指令碼,要將多個檔案合到單個檔案中時有用;
1:刪除所有的注釋;
2:除等級1外,再刪除額外的定位字元和空格;
3:除等級2外,再刪除換行;
4:除等級3外,再進行變數名替換。
ESC擅長把變數名替換成無意義的名稱。它不會更改構造名稱、公用特性和公用方法名稱。
使用ESC時要記住,如果某個JavaScript引用了另一個檔案中的建構函式,4級最佳化會把對建構函式的引用替換成無意義的名稱。解決方案是將兩個檔案合并成一個檔案,這樣就會保持建構函式的名稱。
6.其他減少位元組數的方法
1)替換布爾值
考慮下面的例子
varbFound=false; for(var i=0;i<aTest.length;i++){ if(aTest[i]==vTest) {bFound=true;} }
可以替換為:
varbFound=0;for(var i=0;i if(aTest[i]==vTest) {bFound=1;} }
2)縮短否定檢測
if(oTest !=#ff0000) {//do something}if(oTest !=null) {//do something}if(oTest !=false) {//do something}
雖然這些都正確,但用邏輯非操作符來操作也有同樣的效果
if(!oTest) {//do something}
7.使用數組和對象字面量
varaTest = new Array;var aTest =[];
第二行用了數組字面量,與第一行效果一樣,但要短很多。
類似,對象字面量也可用於節省空間的,以下兩行效果一行,但對象字面量要更簡短
varaTest = new Object;var aTest ={};
如果要建立具有一些特性的一般對象,也可以使用字面量,如下:
varoFruit = new O;oFruit.color="red";oFruit.name="apple";
前面的代碼可用對象字面量來改寫成這樣:
varoFruit = {color:"red",name:"apple"};
一.執行效率
1. DOM
1.1 使用DocumentFragment最佳化多次append
說明:添加多個dom元素時,先將元素append到DocumentFragment中,最後統一將DocumentFragment添加到頁面。
該做法可以減少頁面渲染dom元素的次數。經IE和Fx下測試,在append1000個元素時,效率能提高10%-30%,Fx下提升較為明顯。
服用前:
for (var i = 0; i < 1000; i++) {
var el = document.createElement('p');
el.innerHTML = i;
document.body.appendChild(el);
}
服用後:
var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
var el = document.createElement('p');
el.innerHTML = i;
frag.appendChild(el);
}
document.body.appendChild(frag);
1.2 通過模板元素clone,替代createElement
說明:通過一個模板dom對象cloneNode,效率比直接建立element高。
效能提高不明顯,約為10%左右。在低於100個元素create和append操作時,沒有優勢。
服用前:
var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
var el = document.createElement('p');
el.innerHTML = i;
frag.appendChild(el);
}
document.body.appendChild(frag);
服用後:
var frag = document.createDocumentFragment();
var pEl = document.getElementsByTagName('p')[0];
for (var i = 0; i < 1000; i++) {
var el = pEl.cloneNode(false);
el.innerHTML = i;
frag.appendChild(el);
}
document.body.appendChild(frag);
1.3 使用一次innerHTML賦值代替構建dom元素
說明:根據資料構建列表樣式的時候,使用設定列表容器innerHTML的方式,比構建dom元素並append到頁面中的方式,效率有數量級上的提高。
服用前:
var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
var el = document.createElement('p');
el.innerHTML = i;
frag.appendChild(el);
}
document.body.appendChild(frag);
服用後:
var html = [];
for (var i = 0; i < 1000; i++) {
html.push('<p>' + i + '</p>');
}
document.body.innerHTML = html.join('');
1.4 使用firstChild和nextSibling代替childNodes遍曆dom元素
說明:約能獲得30%-50%的效能提高。逆向遍曆時使用lastChild和previousSibling。
服用前:
var nodes = element.childNodes;
for (var i = 0, l = nodes.length; i < l; i++) {
var node = nodes[i];
……
}
服用後:
var node = element.firstChild;
while (node) {
……
node = node.nextSibling;
}
2. 字串
2.1 使用Array做為StringBuffer,代替字串拼接的操作
說明:IE在對字串拼接的時候,會建立臨時的String對象;經測試,在IE下,當拼接的字串越來越大時,運行效率會急劇下降。Fx和Opera都對字串拼接操作進行了最佳化;經測試,在Fx下,使用Array的join方式執行時間約為直接字串拼接的1.4倍。
服用前:
var now = new Date();
var str = '';
for (var i = 0; i < 10000; i++) {
str += '123456789123456789';
}
alert(new Date() - now);
服用後:
var now = new Date();
var strBuffer = [];
for (var i = 0; i < 10000; i++) {
strBuffer.push('123456789123456789');
}
var str = strBuffer.join('');
alert(new Date() - now);
3. 迴圈語句
3.1 將迴圈控制量儲存到局部變數
說明:對數組和列表對象的遍曆時,提前將length儲存到局部變數中,避免在迴圈的每一步重複取值。
服用前:
var list = document.getElementsByTagName('p');
for (var i = 0; i < list.length; i++) {
……
}
服用後:
var list = document.getElementsByTagName('p');
for (var i = 0, l = list.length; i < l; i++) {
……
}
3.2 順序無關的遍曆時,用while替代for
說明:該方法可以減少局部變數的使用。比起效率最佳化,更能直接看到的是字元數量的最佳化。該做法有程式員強迫症的嫌疑。
服用前:
var arr = [1,2,3,4,5,6,7];
var sum = 0;
for (var i = 0, l = arr.length; i < l; i++) {
sum += arr[i];
}
服用後:
var arr = [1,2,3,4,5,6,7];
var sum = 0, l = arr.length;
while (l--) {
sum += arr[l];
}
4. 條件分支
4.1 將條件分支,按可能性順序從高到低排列
說明:可以減少解譯器對條件的探測次數。
4.2 在同一條件子的多(>2)條件分支時,使用switch優於if
說明:switch分支選擇的效率高於if,在IE下尤為明顯。4分支的測試,IE下switch的執行時間約為if的一半。
4.3 使用三目運算子覆寫準則分支
服用前:
if (a > b) {
num = a;
} else {
num = b;
}
服用後:
num = a > b ? a : b;
5. 定時器
5.1 需要不斷執行的時候,優先考慮使用setInterval
說明:setTimeout每一次都會初始化一個定時器。setInterval只會在開始的時候初始化一個定時器
服用前:
var timeoutTimes = 0;
function timeout () {
timeoutTimes++;
if (timeoutTimes < 10) {
setTimeout(timeout, 10);
}
}
timeout();
服用後:
var intervalTimes = 0;
function interval () {
intervalTimes++;
if (intervalTimes >= 10) {
clearInterval(interv);
}
}
var interv = setInterval(interval, 10);
5.2 使用function而不是string
說明:如果把字串作為setTimeout和setInterval的參數,瀏覽器會先用這個字串構建一個function。
服用前:
var num = 0;
setTimeout('num++', 10);
服用後:
var num = 0;
function addNum () {
num++;
}
setTimeout(addNum, 10);
6. 其他
6.1 盡量不使用動態文法元素
說明:eval、Function、execScript等語句會再次使用javascript解析引擎進行解析,需要消耗大量的執行時間。
6.2 重複使用的調用結果,事先儲存到局部變數
說明:避免多次取值的調用開銷。
服用前:
var h1 = element1.clientHeight + num1;
var h2 = element1.clientHeight + num2;
服用後:
var eleHeight = element1.clientHeight;
var h1 = eleHeight + num1;
var h2 = eleHeight + num2;
6.3 使用直接量
說明:
var a = new Array(param,param,...) -> var a = []
var foo = new Object() -> var foo = {}
var reg = new RegExp() -> var reg = /.../
6.4 避免使用with
說明: with雖然可以縮短代碼量,但是會在運行時構造一個新的scope。
OperaDev上還有這樣的解釋,使用with語句會使得解譯器無法在文法解析階段對代碼進行最佳化。對此說法,無法驗證。
服用前:
with (a.b.c.d) {
property1 = 1;
property2 = 2;
}
服用後:
var obj = a.b.c.d;
obj.property1 = 1;
obj.property2 = 2;
6.5 巧用||和&&布林運算子
重要程度:★★★
服用前:
function eventHandler (e) {
if(!e) e = window.event;
}
服用後:
function eventHandler (e) {
e = e || window.event;
}
服用前:
if (myobj) {
doSomething(myobj);
}
服用後:
myobj && doSomething(myobj);
6.6 類型轉換
說明:
1). 數字轉換成字串,應用"" + 1,效能上:("" +) > String() > .toString() > new String();
2). 浮點數轉換成整型,不使用parseInt(), parseInt()是用於將字串轉換成數字,而不是浮點數和整型之間的轉換,建議使用Math.floor()或者Math.round()
3). 對於自訂的對象,推薦顯式調用toString()。內部操作在嘗試所有可能性之後,會嘗試對象的toString()方法嘗試能否轉化為String。
二.記憶體管理
2.1 循環參考
說明:如果循環參考中包含DOM對象或者ActiveX對象,那麼就會發生記憶體泄露。記憶體泄露的後果是在瀏覽器關閉前,即使是重新整理頁面,這部分記憶體不會被瀏覽器釋放。
簡單的循環參考:
var el = document.getElementById('MyElement');
var func = function () {…}
el.func = func;
func.element = el;
但是通常不會出現這種情況。通常循環參考發生在為dom元素添加閉包作為expendo的時候。
如:
function init() {
var el = document.getElementById('MyElement');
el.onclick = function () {……}
}
init();
init在執行的時候,當前上下文我們叫做context。這個時候,context引用了el,el引用了function,function引用了context。這時候形成了一個循環參考。
下面2種方法可以解決循環參考:
1) 置空dom對象
服用前:
function init() {
var el = document.getElementById('MyElement');
el.onclick = function () {……}
}
init();
服用後:
function init() {
var el = document.getElementById('MyElement');
el.onclick = function () {……}
el = null;
}
init();
將el置空,context中不包含對dom對象的引用,從而打斷迴圈應用。
如果我們需要將dom對象返回,可以用如下方法:
服用前:
function init() {
var el = document.getElementById('MyElement');
el.onclick = function () {……}
return el;
}
init();
服用後:
function init() {
var el = document.getElementById('MyElement');
el.onclick = function () {……}
try{
return el;
} finally {
el = null;
}
}
init();
2) 構造新的context
服用前:
function init() {
var el = document.getElementById('MyElement');
el.onclick = function () {……}
}
init();
服用後:
function elClickHandler() {……}
function init() {
var el = document.getElementById('MyElement');
el.onclick = elClickHandler;
}
init();
把function抽到新的context中,這樣,function的context就不包含對el的引用,從而打斷循環參考。
2.2 通過javascript建立的dom對象,必須append到頁面中
說明:IE下,指令碼建立的dom對象,如果沒有append到頁面中,重新整理頁面,這部分記憶體是不會回收的!
範例程式碼:
function create () {
var gc = document.getElementById('GC');
for (var i = 0; i < 5000 ; i++)
{
var el = document.createElement('div');
el.innerHTML = "test";
//下面這句可以注釋掉,看看瀏覽器在工作管理員中,點擊按鈕然後重新整理後的記憶體變化
gc.appendChild(el);
}
}
2.3 釋放dom元素佔用的記憶體
說明:
將dom元素的innerHTML設定為空白字串,可以釋放其子項目佔用的記憶體。
在rich應用中,使用者也許會在一個頁面上停留很長時間,可以使用該方法釋放積累得越來越多的dom元素使用的記憶體。
2.4 釋放javascript對象
說明:在rich應用中,隨著執行個體化對象數量的增加,記憶體消耗會越來越大。所以應當及時釋放對對象的引用,讓GC能夠回收這些記憶體控制項。
對象:obj = null
對象屬性:delete obj.myproperty
數組item:使用數組的splice方法釋放數組中不用的item
2.5 避免string的隱式裝箱
說明:對string的方法調用,比如'xxx'.length,瀏覽器會進行一個隱式的裝箱操作,將字串先轉換成一個String對象。推薦對聲明有可能使用String執行個體方法的字串時,採用如下寫法:
var myString = new String('Hello World');
語言相關:
迴圈
迴圈是很常用的一個控制結構,大部分東西要依靠它來完成,在JavaScript中,我們可以使用for(;;),while(),for(in)三種迴圈,事實上,這三種迴圈中for(in)的效率極差,因為他需要查詢散列鍵,只要可以,就應該盡量少用。for(;;)和while迴圈,while迴圈的效率要優於for(;;),可能是因為for(;;)結構的問題,需要經常跳回去。
局部變數和全域變數
局部變數的速度要比全域變數的訪問速度更快,因為全域變數其實是全域對象的成員,而局部變數是放在函數的棧當中的。
不使用Eval
使用eval相當於在運行時再次調用解釋引擎對內容進行運行,需要消耗大量時間。這時候使用JavaScript所支援的閉包可以實現函數模版(關於閉包的內容請參考函數式編程的有關內容)
減少對象尋找
因為JavaScript的解釋性,所以a.b.c.d.e,需要進行至少4次查詢操作,先檢查a再檢查a中的b,再檢查b中的c,如此往下。所以如果這樣的運算式重複出現,只要可能,應該盡量少出現這樣的運算式,可以利用局部變數,把它放入一個臨時的地方進行查詢。
這一點可以和迴圈結合起來,因為我們常常要根據字串、數組的長度進行迴圈,而通常這個長度是不變的,比如每次查詢a.length,就要額外進行一個操作,而預先把var len=a.length,則就少了一次查詢。
字串串連
如果是追加字串,最好使用s+=anotherStr操作,而不是要使用s=s+anotherStr。
如果要串連多個字串,應該少使用+=,如
s+=a;
s+=b;
s+=c;
應該寫成s+=a + b + c;
而如果是收集字串,比如多次對同一個字串進行+=操作的話,最好使用一個緩衝。怎麼用呢?使用JavaScript數組來收集,最後使用join方法串連起來,如下
var buf = new Array();
for(var i = 0; i < 100; i++){
buf.push(i.toString());
}
var all = buf.join("");
類型轉換
類型轉換是大家常犯的錯誤,因為JavaScript是動態類型語言,你不能指定變數的類型。
1. 把數字轉換成字串,應用"" + 1,雖然看起來比較醜一點,但事實上這個效率是最高的,效能上來說:
("" +) > String() > .toString() > new String()
這條其實和下面的“直接量”有點類似,盡量使用編譯時間就能使用的內部操作要比運行時使用的使用者操作要快。
String()屬於內建函式,所以速度很快,而.toString()要查詢原型中的函數,所以速度遜色一些,new String()用於返回一個精確的副本。
2. 浮點數轉換成整型,這個更容易出錯,很多人喜歡使用parseInt(),其實parseInt()是用於將字串轉換成數字,而不是浮點數和整型之間的轉換,我們應該使用Math.floor()或者Math.round()。
另外,和第二節的對象尋找中的問題不一樣,Math是內部對象,所以Math.floor()其實並沒有多少查詢方法和調用的時間,速度是最快的。
3. 對於自訂的對象,如果定義了toString()方法來進行類型轉換的話,推薦顯式調用toString(),因為內部的操作在嘗試所有可能性之後,會嘗試對象的toString()方法嘗試能否轉化為String,所以直接調用這個方法效率會更高
使用直接量
其實這個影響倒比較小,可以忽略。什麼叫使用直接量,比如,JavaScript支援使用[param,param,param,...]來直接表達一個數組,以往我們都使用new Array(param,param,...),使用前者是引擎直接解釋的,後者要調用一個Array內部構造器,所以要略微快一點點。
同樣,var foo = {}的方式也比var foo = new Object();快,var reg = /../;要比var reg=new RegExp()快。
字串遍曆操作
對字串進行迴圈操作,譬如替換、尋找,應使用Regex,因為本身JavaScript的迴圈速度就比較慢,而Regex的操作是用C寫成的語言的API,效能很好。
進階對象
自訂進階對象和Date、RegExp對象在構造時都會消耗大量時間。如果可以複用,應採用緩衝的方式。
DOM相關:
插入HTML
很多人喜歡在JavaScript中使用document.write來給頁面產生內容。事實上這樣的效率較低,如果需要直接插入HTML,可以找一個容器元素,比如指定一個div或者span,並設定他們的innerHTML來將自己的HTML代碼插入到頁面中。
物件查詢
使用[“”]查詢要比.items()更快,這和前面的減少對象尋找的思路是一樣的,調用.items()增加了一次查詢和函數的調用。
建立DOM節點
通常我們可能會使用字串直接寫HTML來建立節點,其實這樣做
無法保證代碼的有效性
字串操作效率低
所以應該是用document.createElement()方法,而如果文檔中存在現成的樣板節點,應該是用cloneNode()方法,因為使用createElement()方法之後,你需要設定多次元素的屬性,使用cloneNode()則可以減少屬性的設定次數——同樣如果需要建立很多元素,應該先準備一個樣板節點。
定時器
如果針對的是不斷啟動並執行代碼,不應該使用setTimeout,而應該是用setInterval。setTimeout每次要重新設定一個定時器。
其他:
指令碼引擎
據我測試Microsoft的JScript的效率較Mozilla的Spidermonkey要差很多,無論是執行速度還是記憶體管理上,因為JScript現在基本也不更新了。但SpiderMonkey不能使用ActiveXObject
檔案最佳化
檔案最佳化也是一個很有效手段,刪除所有的空格和注釋,把代碼放入一行內,可以加快下載的速度,注意,是下載的速度而不是解析的速度,如果是本地,注釋和空格並不會影響解釋和執行速度。
批量增加Dom
盡量使用修改innerHTML的方式而不是用appendChild的方式; 因為使用innerHTML開銷更小,速度更快,同時也更加記憶體安全.
有一點需要注意的是,用innerHTML方式添加時,一定不要在迴圈中使用 innerHTML += 的方式添加,這樣反而會使速度減慢; 而是應該中間用array緩衝起來,迴圈結束後調用 xx.innerHTML = array.join(‘’);的方式,或者至少儲存到string中再插到innerHTML中.
針對使用者列表一塊採用這種方式最佳化後,載入速度提升一倍.
單個增加Dom
這裡是指要將新節點載入到一個內容不斷變化的節點的情形,對於內容穩定的節點來說,隨便怎麼加都沒有問題. 但是對於有動態內容的節點來說,為其添加子節點盡量使用 dom append的方式.
這是因為,dom append不會影響到其他的節點;而如果修改innerHTML 屬性的話,該父節點的所有子節點都會從dom樹中剝離,再根據新的innerHTML值來重繪子節點dom樹;所有註冊到原來子節點的事件也會失效.
綜上,如果在一個有動態內容的節點上 出現了 innerHTML += 的代碼,就該考慮是否有問題了.
建立Dom節點
用createElement方式建立一個dom節點,有一個很重要的細節: 在執行完createElement代碼之後,應該馬上append到dom樹中; 否則,如果在將這個孤立節點載入到dom樹之前所做的賦值它的屬性和innerHTML的操作都會引發該dom片段記憶體無法回收的問題. 這個不起眼細節,一旦遇到大量dom增刪操作,就會引發記憶體的災難.
刪除Dom節點
刪除dom節點之前,一定要刪除註冊在該節點上的事件,不管是用observe方式還是用attachEvent方式註冊的事件,否則將會產生無法回收的記憶體.
另,在removeChild和innerHTML=’’二者之間,盡量選擇後者. 因為在sIEve(記憶體泄露監測工具)中監測的結果是用removeChild無法有效地釋放dom節點.
建立事件監聽
現有的js庫都採用observe方式來建立事件監聽,其實現上隔離了dom對象和事件處理函數之間的循環參考,所以應該盡量採用這種方式來建立事件監聽.
監聽動態元素
Dom事件預設是向上冒泡的,發生在子節點中的事件,可以由父節點來處理. Event的 target/srcElement 仍是產生事件的最深層子節點. 這樣,對於內容動態增加並且子節點都需要相同的事件處理函數的情況,可以把事件註冊上提到父節點上,這樣就不需要為每個子節點註冊事件監聽了.
同時,這樣做也避免了產生無法回收的記憶體.即使是用Prototype的observe方式註冊事件並在刪除節點前調用stopObserving,也會產生出少量無法回收的記憶體,所以應該盡量少的為dom節點註冊事件監聽.
所以,當代碼中出現在迴圈裡註冊事件時,也是我們該考慮事件上提機制的時候了.
HTML提純
HTML提純體現的是一種各負其責的思想. HTML只用來顯示,盡量不出現和顯示無關的屬性.比如onclick事件,比如自訂的對象屬性.
事件可以用前面的方法避免, 對象屬性指的是這樣的一種情景: 通常情況下,動態增加的內容都是有個對象和它對應,比如聊天室的使用者列表,每個顯示使用者的dom節點都有一個user對象和它對應,這樣在html中,應該僅保留一個id屬性和user對象對應,而其他的資訊,則應通過user對象去擷取.
另外注意:JavaScript在IE下的記憶體流失問題