轉載 -- 前端效能優化指南
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)的操作。