前端性能優化(JavaScript 篇)

1.優化循環

如果現在有一個 data[] 數組,需要對其進行遍歷,應當怎麼做?最簡單的代碼是:

for(let i = 0; i < data.length; i++){
}

這裏每次循環開始都需要判斷 i 是否小於 data.length, JavaScript 並不會對 data.length 進行緩存,而是每次比較都會進行一次取值,如我們所說,JavaScript 數組其實是一個對象,裏面有一個 length 屬性,所以這裏實際上就是取得對象的屬性,如果直接使用變量的話就會少一次索引對象,如果數組的元素很多,效率提升還是很可觀的,所以我們通常將代碼改成如下所示:

for(let i = 0, m = data.length; i < m; i++){
}

這裏多加了一個變量 m 用於存放 data.length 屬性,這樣就可以在每次循環時,減少一次索引對象,但是代價是增加了一個變量空間,如果遍歷不要求順序,我們甚至可以不用 m 這個變量存儲長度,在不要求順序的時候可以使用如下代碼:

for(let i = data.length; i--; ){
}
// 當然 也可以用 while 來代替
let i = data.length;
while(i--){
}

這樣就只使用一個變量了。

2.運算結果緩存

由於JavaScript 中的函數也是對象(JavaScript中一切都是對象),所以我們可以給函數添加任意屬性,這也就爲我們提供符合備忘錄模式的緩存運算結果的功能,如果我們有一個需要大量運算才能得到結果的函數如下:

function calculator(params){
	// 大量的耗時的計算
	return result;
}

如果其中不涉及隨機,參數一樣時所返回的結果一致,我們就可以將運算結果進行緩存,從而避免重複的計算:

function calculator(params){
	let cacheKey = JSON.stringify(params);
	let cache = calculator.cache = calculator.cache || {};
	if(typeof cache[cacheKey] !== 'undefined'){
		return cache[cacheKey];
	}
	// 大量的計算
	cache[cacheKey] = result;
	return result;
}

這裏將參數轉化爲 JSON 字符串作爲 key,如果這個參數已經被計算過,那麼就直接返回,否則進行計算,計算完畢後再添加 cache 中,如果需要,可以直接查看 cache 的內容:calculator.cache
這是一種很典型的空間換時間的方式,由於瀏覽器的頁面存活時間一般不會很長,佔用的內存會很快被釋放(當然也有例外,比如一些 WEB 應用),所以可以通過這種空間換時間的方式來減少響應時間,提升用戶體驗,這種方式不適合如下場合:

  • 相同的參數可能 產生不同的結果(包含隨機數之類的)
  • 運算結果佔用特別多的內存的情況

3.不要在循環中創建函數

這個很好理解,每創建一個函數對象是需要大批量空間的,所以在一個循環中創建函數是不明智的,儘量將函數移動到循環之外創建,比如如下代碼:

for(let i = 0, m = data.length; i < m; i++){
	handlerData(data[i],function(data){
	  // do something
	})
}
// 這個可以 修改爲:
let handler = function(data){
	// do something
}
for(let i = 0, m = data.length; i < m ; i ++){
	handlerData(data[i], handler)
}

4.讓垃圾回收器回收那些不再需要的對象

如果我們長時間保存對象,老生代中佔用的空間將增大,每次在老生代中的垃圾回收過程將會相當漫長,而垃圾回收器判斷一個對象爲活對象還是死對象,是按照是否有活對象或根對象含有對它的引用來判定的,如果有根對象或者活對象引用了這個對象,它將被判斷爲活對象,所以我們需要通過手動消除這些引用來讓垃圾回收器來回收這些對象。

delete
一種方式是通過 delete 方式來消除對象中的鍵值對,從而消除引用,但是這種方式並不提倡,它會改變對象的結構,可能導致引擎中對對象的的存儲方式變更,降級爲字典方式進行存儲,不利於 JavaScript 引擎的優化,所以儘量減少使用。

全局對象
另外需要注意的是,垃圾回收器認爲根對象永遠是活對象,永遠不會對其進行垃圾回收,而全局對象就是根對象,所以全局作用域中的變量將會一直存在。

事件處理器的回收
在平常寫代碼的時候,我們經常會給一個 DOM 節點綁定事件處理器,但有時候我們不需要這些事件處理器後,就不管它們了,它們默默的在內存中保存着,所以在某些 DOM 節點綁定的事件處理器不需要後,我們應當銷燬它們,同時綁定的時候也儘量使用事件代理的方式進行綁定,以免造成多次重複的綁定導致內存空間的浪費。
閉包導致的內存泄漏
JavaScript 的閉包可以說即是“天使”又是“魔鬼”,它天使的一面是我們可以通過它突破作用域的限制,而其魔鬼的一面就是容易導致內存泄漏,比如如下情況:

let result = (function(){
	let small = {};
	let big = new Array(10000000);
	return function(){
		if(big.indexOf('someValue') !== -1){
			return null;
		}else{
			return small;
		}
	}
})()

這裏,創建了一個閉包,使得返回的函數存儲在 result 中,而 result 函數能夠訪問其作用域內的 small 對象和 big 對象。由於 big 對象和 small 對象都可能被訪問,所以垃圾回收器不會去碰這兩個對象,它們不會被回收,我們上述代碼改爲下述形式:

let result = (function(){
	let small = {};
	let big = new Array(10000000);
	let hasSomeValue;
	hasSomeValue = big.indexOf('someValue') !== -1;
	return function(){
		if(hasSomeValue){
			return null;
		}else{
			return small;
		}
	}
})

這樣,函數內部只能夠訪問到 hasSomeValue 變量和 small 變量了,big 沒有辦法通過任何形式被訪問到,垃圾回收器將會對齊進行回收,節省了大量的內存。

5.慎用 eval 和 with

Douglas Crockford 將 eval 比作魔鬼,確實在很多方面我們可以找到更好的替代方式,使用它時需要在運行時調用解釋引擎對 eval() 函數內部的字符串進行解釋運行,這需要大量的時間,像 Function、setInterval、setTimeout 也是類似的

Douglas Crockford也不建議使用 with, with 會降低性能,通過 with 包裹的代碼塊,作用域將會額外增加一層,降低索引效率。

6.對象的優化

緩存需要被使用的對象
JavaScript 獲取數據的性能如下(從快到慢): 變量獲取 > 數組下標獲取(對象的整數索引獲取)> 對象屬性獲取(對象非整數索引獲取)。我們可以通過最快的方式代替最慢的方式:

let body = document.body;
let maxLength = someArray.length;

需要考慮,作用域鏈和原型鏈中的對象索引,如果作用域和原型鏈較長,也需要對所需要的變量繼續緩存,否則沿着作用域鏈和原型鏈向上查找時也會消耗額外的時間。

緩存正則表達式對象
需要注意,正則表達式對象的創建非常消耗時間,儘量不要在循環中創建正則表達式,儘可能多的對正則表達式對象進行復用。

考慮對象和數組
在 JavaScript 中我們可以使用兩種存放數據:對象和數組,由於 JavaScript 數組可以存放任意類型數據這樣的靈活性,導致我們經常需要考慮何時使用數組,何時使用對象,我們應當在如下情況作出考慮:

  1. 存儲一串相同類型的對象,應當使用數組
  2. 存儲一對鍵值對,值得類型多樣性,應當使用對象
  3. 所有值都是通過整數索引,應當使用數組

數組使用時的優化

  1. 往數組中插入混合類型很容易降低數組使用的效率,儘量保持數組中元素的類型一致。
  2. 如果使用稀疏數組,它的元素訪問將遠慢於滿數組的元素訪問,因爲 V8 爲了節省空間,會將對稀疏數組通過字典方式保存在內存中,節約了空間,但增加了訪問時間。

對象的拷貝
需要注意的是,JavaScript 遍歷對象和數組時,使用的是 for…in 的效率相當低,所以在拷貝對象時,如果已知需要被拷貝的對象的屬性,通過直接賦值的方法比使用 for…in 方式要來得快,我們可以通過設定一個拷貝構造函數來實現,如下:

function copy(source){
	let result = {};
	let item;
	for(item in source){
		result[item] = source[item];
	}
	return result;
}
let backup = copy(source);
// 可修改爲 
function copy(source){
	this.property1 = source.property1;
	this.property2 = source.property2;
	this.property3 = source.property3;
	// ...
}
let backup2 = new Copy(source)

字面量代替構造函數
JavaScript 可以通過字面量來構造對象,比如通過 [] 構造一個數組, {} 構造一個對象, /regexp/ 構造一個正則表達式,我們應當盡力使用字面量來構造對象,因爲字面量是引擎直接解釋執行的,而如果使用構造函數的話,需要調用一個內部構造器,所以字面量略微要快一點點。

緩存Ajax

(1)函數緩存
我們可以使用前面緩存複雜計算函數接的方式進行緩存,通過在函數對象上構造 cache 對象,原理一樣,這裏略過,這種方式是精確到函數,而不是精確到請求。
(2)本地緩存
HTML5 提供了本地緩存 sessionStroage 和 localStorage,區別就在於前者在瀏覽器關閉後會自動釋放,而後者則是永久的,不會被釋放,它提供的緩存大小是以 MB 爲單位的,比 cookie 要大的多,所以我們可以提供 Ajax 數據緩存的存活時間來判斷存放在 sessionStorage 還是 localStorage 當中,在這裏以存儲到 sessionStorage 中爲例:

function(data, url, type, callback){
	let storage = window.sessionStorage;
	let key = JSON.stringify({
		url:url,
		type:type,
		data:data
	});
	let result = storage.getItem(key);
	let xhr;
	if(result){
		callback.call(null, result);
	}else{
		xhr.onreadystatechange = function(){
			if(xhr.readyState === 4){
				if(xhr.status === 200){
					storage.setItem(key,xhr.responseText);
					callback.call(null,xhr.responseText);
				}else{}
			}
		}
		xhr.open(type, url, async);
		xhr.send(data);
	}
}

使用布爾表達式的短路
使用原生方法
在 JavaScript 中,大多數原生方法是使用 C++ 編寫的,比 JS寫的方法要快得多,所以儘量使用原生對象和方法。

字符串拼接
在 IE 和 FF 下,使用直接 += 的方式或是 + 的方式進行字符串拼接,將會很慢,我們可以通過 Array 的 join() 方法進行字符串拼接,不過並不是所有的瀏覽器都是這樣,現在很多瀏覽器使用 += 比 join() 方法還要快。

4.JavaScript 文件的優化

使用 CDN
在編寫 JavaScirpt 代碼中,我們會經常使用庫(jQuery等等),這些 JS 庫通常不會對其進行更改,我們可以將這些庫文件放在 CDN (內容分發網絡上),這樣能大大減少響應時間。

壓縮與合併 JavaScript 文件
在網絡只不過傳輸 JS 文件,文件越長,需要的時間越多,所以在上線前,通常都會對 JS 文件進行壓縮,去掉其中的註釋、回車、不必要的空格等多餘內容,如果通過 uglify 的算法,還可以縮減變量名和函數名,從而將 JS 代碼壓縮,節約傳輸帶寬,另外經常也會將 JavaScript 代碼合併,使所有代碼在一個文件之中,這樣就能夠減少 HTTP 請求次數,合併的原理和 sprite 技術相同

使用 Application Cache 緩存

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章