最近在閱讀這本Nicholas C.Zakas(javascript高級程序設計作者)寫的最佳實踐、性能優化類的書。記錄下主要知識。
加載和執行
腳本位置
放在<head>中的javascript文件會阻塞頁面渲染:一般來說瀏覽器中有多種線程:UI渲染線程、javascript引擎線程、瀏覽器事件觸發線程、HTTP請求線程等。多線程之間會共享運行資源,瀏覽器的js會操作dom,影響渲染,所以js引擎線程和UI渲染線程是互斥的,導致執行js時會阻塞頁面的渲染。
<b>最佳實踐:所有的script標簽應盡可能的放在body標簽的底部,以盡量減少對整個頁面下載的影響。</b>
組織腳本
每個<script>標簽初始下載時都會阻塞頁面渲染,所以應減少頁面包含的<script>標簽數量。內嵌腳本放在引用外鏈樣式表的<link>標簽之后會導致頁面阻塞去等待樣式表的下載,建議不要把內嵌腳本緊跟在<link>標簽之后。外鏈javascript的HTTP請求還會帶來額外的性能開銷,減少腳本文件的數量將會改善性能。
無阻塞的腳本
無阻塞腳本的意義在于在頁面加載完成后才加載javascript代碼。(window對象的load事件觸發后)
延遲的腳本
帶有defer屬性的<script>標簽可以放置在文檔的任何位置。對應的javascript文件將在頁面解析到<script>標簽時開始下載,但并不會執行,直到DOM加載完成(onload事件被觸發前)。當一個帶有defer屬性的javascript文件下載時,它不會阻塞瀏覽器的其他進程,可以與其他資源并行下載。執行的順序是script、defer、load。
動態腳本元素
使用javascript動態創建HTML中script元素,例如一些懶加載庫。
<b>優點:動態腳本加載憑借著它在跨瀏覽器兼容性和易用的有時,成為最通用的無阻塞加載解決方式。</b>
XHR腳本注入
創建XHR對線個,用它下載javascript文件,通過動態創建script元素將代碼注入頁面中
var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function() {
if(xht.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
var script = document.createElement("script");
script.type = "text/javascript";
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
};
xhr.send(null);
<b>優點:可以下載javascript但不立即執行,在所有主流瀏覽器中都可以正常工作。
缺點:javascript文件必須與所請求的頁面處于相同的域,意味著不能文件不能從CDN下載。</b>
數據存取
存儲的位置
數據存儲的位置會很大程度上影響讀取速度。
- 字面量:字面量只代表自身,不存儲在特定的位置。包括:字符串、數字、布爾值、對象、數組、函數、正則表達式、null、undefined。(個人理解:對象的指針本身是字面量)
- 本地變量:var定義的數據存儲單元。
- 數組元素:存儲在javascript數組內部,以數字為引。
- 對象成員:存儲在javascript對象內部,以字符串作為索引。
大多數情況下從一個字面量和一個局部變量中存取數據的差距是微不足道的。訪問數據元素和對象成員的代價則高一點。如果在乎運行速度,盡量使用字面量和局部變量,減少數組和對象成員的使用。
管理作用域
作用域鏈
每個javascript函數都表示為一個對象,更確切的說是Function對象的一個實例。它也有僅供javascript引擎存儲的內部屬性,其中一個內部屬性是[[Scope]],包含了一個被創建的作用域中對象的集合即作用域鏈。作用域鏈決定哪些數據能被函數訪問。作用域中的每個對象被稱為一個可變對象。
當一個函數被創建后,作用域鏈會被創建函數的作用域中可訪問的數據對象所填充。執行函數時會創建一個稱為執行上下文的內部對象。執行上下文定義了函數執行時的環境。每次函數執行時對應的執行環境都是獨一無二的,多次調用同一個函數也會創建多個執行上下文,當函數執行完畢,執行上下文就會被銷毀。每個執行上下文都有自己的作用域鏈,用于解析標識符。當執行上下文被創建時,它的作用域鏈初始化為當前運行函數的[[Scope]]屬性中的對象。這些值按照它們出現在函數中的順序,被復制到執行環境的作用域鏈中。這個過程一旦完成,一個被稱為活動對象的新對象就為執行上下文創建好了。
活動對象作為函數運行時的變量對象,包含了所有局部對象,命名函數,參數集合以及this。然后此對象被推入作用域鏈的最前端。當執行環境被銷毀時,活動對象也隨之銷毀。執行過程中,每遇到一個變量,都會經歷一次標識符解析過程以決定從哪里獲取或存儲數據。該過程搜索執行環境的作用域鏈,查找同名的標識符。搜索過程從作用域鏈頭部開始,也就是當前運行函數的活動對象。如果找到,就使用這個標識符對應的變量,如果沒找到,繼續搜索作用域鏈的下一個對象知道找到,若無法搜索到匹配的對象,則標識符被當作未定義的。這個搜索過程影響了性能。
標識符解析的性能
一個標識符所在的位置越深,讀寫速度就越慢,全局變量總是存在于執行環境作用域的最末端,因此它是最深的。
<b>最佳實踐:如果某個跨作用域的值在函數中被引用一次以上,那么就把它存儲到局部變量中。</b>
改變作用域鏈
一般來說一個執行上下文的作用域鏈是不會改變的。但是,with語句和try-catch語句的catch子語句可以改變作用域鏈。
with語句用來給對象的所有屬性創建一個變量,可以避免多次書寫。但是存在性能問題:代碼執行到with語句時,執行環境的作用域鏈臨時被改變了,創建了一個新的(包含了with對象所有屬性)對象被創建了,之前所有的局部變量現在處于第二個作用域鏈對象中,提高了訪問的代價。建議放棄使用with語句。
try-catch語句中的catch子句也可以改變作用域鏈,當try代碼塊中發生錯誤,執行過程會自動跳轉到catch子句,把異常對象推入一個變量對象并置于作用域的首位,局部變量處于第二個作用域鏈對象中。簡化代碼可以使catch子句對性能的影響降低。
<b>最佳實踐:將錯誤委托給一個函數來處理。</b>
動態作用域
無論with語句還是try-catch語句的子句catch子句、eval()語句,都被認為是動態作用域。經過優化的javascript引擎,嘗試通過分析代碼來確定哪些變量是可以在特定的時候被訪問,避開了傳統的作用域鏈,取代以標識符索引的方式快速查找。當涉及動態作用域時,這種優化方式就失效了。
<b>最佳實踐:只在確實有必要時使用動態作用域。</b>
閉包、作用域和內存
由于閉包的[[Scope]]屬性包含了與執行上下文作用域鏈相同的對象的引用,因此會產生副作用。通常來說,函數的活動對象會隨著執行環境一同銷毀。但引入閉包時,由于引用仍然存在閉包的[[Scope]]屬性中,因此激活對象無法被銷毀,導致更多的內存開銷。
最需要關注的性能點:閉包頻繁訪問跨作用域的標識符,每次訪問都會帶來性能損失。
<b>最佳實踐:將常用的跨作用域變量存儲在局部變量中,然后直接訪問局部變量。</b>
對象成員
無論是通過創建自定義對象還是使用內置對象都會導致頻繁的訪問對象成員。
原型
javascript中的對象是基于原型的。解析對象成員的過程與解析變量十分相似,會從對象的實例開始,如果實例中沒有,會一直沿著原型鏈向上搜索,直到找到或者到原型鏈的盡頭。對象在原型鏈中位置越深,找到它也就越慢。搜索實例成員比從字面量或局部變量中讀取數據代價更高,再加上遍歷原型鏈帶來的開銷,這讓性能問題更為嚴重。
嵌套成員
對象成員可能包含其他成員,每次遇到點操作符"."會導致javascript引擎搜索所有對象成員。
緩存對象成員值
由于所有類似的性能問題都與對象成員有關,因此應該盡可能避免使用他們,只在必要時使用對象成員,例如,在同一個函數中沒有必要多次讀取同一個對象屬性(保存到局部變量中),除非它的值變了。這種方法不推薦用于對象的方法,因為將對象方法保存在局部變量中會導致this綁定到window,導致javascript引擎無法正確的解析它的對象成員,進而導致程序出錯。
DOM編程
瀏覽器中的DOM
文檔對象模型(DOM)是一個獨立于語言的,用于操作XML和HTML文檔的程序接口API。DOM是個與語言無關的API,在瀏覽器中的接口是用javascript實現的。客戶端腳本編程大多數時候是在和底層文檔打交道,DOM就成為現在javascript編碼中的重要組成部分。瀏覽器把DOM和javascript單獨實現,使用不同的引擎。
天生就慢
DOM和javascript就像兩個島嶼通過收費橋梁連接,每次通過都要繳納“過橋費”。
<b>推薦的做法是盡可能減少過橋的次數,努力待在ECMAScript島上。</b>
DOM訪問與修改
訪問DOM元素是有代價的——前面的提到的“過橋費”。修改元素則更為昂貴,因為它會導致瀏覽器重新計算頁面的幾何變化(重排)。最壞的情況是在循環中訪問或修改元素,尤其是對HTML元素集合循環操作。
<b>在循環訪問頁面元素的內容時,最佳實踐是用局部變量存儲修改中的內容,在循環結束后一次性寫入。</b>
<b>通用的經驗法則是:減少訪問DOM的次數,把運算盡量留在ECMAScript中處理。</b>
節點克隆
大多數瀏覽器中使用節點克隆都比創建新元素要更有效率。
選擇API
使用css選擇器也是一種定位節點的便利途徑,瀏覽器提供了一個名為querySelectorAll()的原生DOM方法。這種方法比使用javascript和DOM來遍歷查找元素快很多。使用另一個便利方法——querySelector()來獲取第一個匹配的節點。
重繪與重排
瀏覽器下載完頁面中的所有組件——HTML標記、javascript、CSS、圖片——之后會解析并生成兩個內部的數據結構:DOM樹(表示頁面結構)、渲染樹(表示DOM節點如何顯示)。當DOM的變化影響了元素的幾何屬性,瀏覽器會使渲染樹中受到影響的部分失效,并重構,這個過程成為重排,完成后,會重新繪制受影響的部分到屏幕,該過程叫重繪。并不是所有的DOM變化都會影響幾何屬性,這時只發生重繪。重繪和重排會導致web應用程序的UI反應遲鈍,應該盡量避免。
重排何時發生
當頁面布局的幾何屬性改變時就需要重排:
1. 添加或刪除可見的DOM元素
2. 元素位置改變
3. 元素尺寸改變(包括:外邊據、內邊距、邊框厚度、寬度、高度等屬性改變)
4. 內容改變,例如:文本改變或圖片被另一個不同尺寸的圖片代替
5. 頁面渲染器初始化
6. 瀏覽器窗口尺寸改變
渲染樹變化的排隊與刷新
由于每次重排都會產生計算消耗,大多數瀏覽器通過隊列化修改并批量執行來優化重排過程。但是有些操作會導致強制刷新隊列并要求任務立刻執行:
1. offsetTop,offsetLeft,offsetWidth,offsetHeight
2. scrollTop,scrollLeft,scrollWidth,scrollHeight
3. clientTop,clientLeft,clientWidth,clientHeight
4. getComputedStyle()
以上屬性和方法需要返回最新的布局信息,因此瀏覽器不得不執行渲染隊列中的修改變化并觸發重排以返回正確的值。
<b>最佳實踐:盡量將修改語句放在一起,查詢語句放在一起。</b>
最小化重繪和重排
為了減少發生次數,應該合并多次DOM的樣式的修改,然后一次處理掉。
批量修改DOM
當你需要對DOM元素進行一系列操作時,可以通過以下步驟來減少重繪和重排的次數:
1. 使元素脫離文檔
2. 對其應用多重改變
3. 把元素帶回文檔流
該過程會觸發兩次重排——第一步和第三步,如果忽略這兩步,在第二步所產生的任何修改都會觸發一次重排。
有三種基本的方法可以使DOM脫離文檔:
1. 隱藏元素,應用修改,重新顯示
2. 使用文檔片段,在當前DOM之外構建一個子樹,再把它拷貝回文檔
3. 將原始元素拷貝到一個脫離文檔的節點中,修改副本,完成后再替換原始元素
<b>推薦使用文檔片段,因為它們所產生的DOM遍歷和重排次數最少。</b>
緩存緩存布局信息
當你查詢布局信息時,瀏覽器為了返回最新值,會刷新隊列并應用所有變更。
<b>最佳實踐:盡量減少布局信息的獲取次數,獲取后把它賦值給局部變量,然后操作局部變量。</b>
讓元素脫離動畫流
用展開、折疊的方式來顯示和隱藏部分頁面是一種常見的交互模式。通常包括展開區域的幾何動畫,并將頁面其他部分推向下方。一般來說,重排只影響渲染樹中的一小部分,但也可能影響很大的部分,甚至整個渲染樹。瀏覽器所需要重排的次數越少,應用程序的響應速度就越快。當一個動畫改變整個頁面的余下部分時,會導致大規模重排。節點越多情況越差。避免大規模的重排:
1. 使用絕對定位頁面上的動畫元素,將其脫離文檔流。
2. 應用動畫
3. 當動畫結束時回恢復定位,從而只會下移一次文檔的其他元素。
這樣只造成了頁面的一個小區域的重繪,不會產生重排并重繪頁面的大部分內容。
:hover
如果有大量元素使用了:hover,那么會降低響應速度。此問題在IE8中更為明顯。
事件委托
當頁面中存在大量元素,并且每一個都要一次或多次綁定事件處理器時,這種情況可能會影響性能,每綁定一個事件處理器都是有代價的,它要么加重了頁面負擔(更多的代碼、標簽),要么增加了運行期的執行時間。需要訪問和修改的DOM元素越多,應用程序就越慢,特別是事件綁定通常發生在onload時,此時對每一個富交互應用的網頁來說都是一個擁堵的時刻。事件綁定占用了處理事件,而且瀏覽器要跟蹤每個事件處理器,這也會占用更多的內存。這些事件處理器中的絕大部分都可能不會被觸發。
<b>事件委托原理:事件逐層冒泡并能被父級元素捕獲。使用事件代理,只需要給外層元素綁定一個處理器,就可以處理在其子元素上觸發的所有事件。</b>
根據DOM標準,每個事件都要經歷三個階段:
1. 捕獲
2. 到達目標
3. 冒泡
IE不支持捕獲,但是對于委托而言,冒泡已經足夠。
<body>
<div>
<ul id="menu">
<li>
<a href="menu1.html">menu #1</a>
</li>
<li>
<a href="menu1.html">menu #2</a>
</li>
</ul>
</div>
</body>
在以上的代碼中,當用戶點擊鏈接“menu #1”,點擊事件首先從a標簽元素收到,然后向DOM樹上層冒泡,被li標簽接收然后是ul標簽然后是div標簽,一直到達document的頂層甚至window。
委托實例:阻止默認行為(打開鏈接),只需要給所有鏈接的外層UL"menu"元素添加一個點擊監聽器,它會捕獲并分析點擊是否來自鏈接。
document.getElementById('menu').onclick = function(e) {
//瀏覽器target
e=e||window.event;
var target = e.target||e.srcElement;
var pageid,hrefparts;
//只關心hrefs,非鏈接點擊則退出,注意此處是大寫
if (target.nodeName !== 'A') {
return;
}
//從鏈接中找出頁面ID
hrefparts = target.href.split('/');
pageid = hrefparts[hrefparts.length-1];
pageid = pageid.replace('.html','');
//更新頁面
ajaxRequest('xhr.php?page='+id,updatePageContents);
//瀏覽器阻止默認行為并取消冒泡
if (type of e.preventDefault === 'function') {
e.preventDefault();
e.stopPropagation();
} else {
e.returnValue=false;
e.cancelBubble=true;
}
};
跨瀏覽器兼容部分:
1. 訪問事件對象,并判斷事件源
2. 取消文檔樹中的冒泡(可選)
3. 阻止默認動作(可選)
算法和流程控制
循環
循環的類型
ECMA-262標準第三版定義了javascript的基本語法和行為,其中共有四種循環。
1. 第一種是標準的for循環。它由四部分組成:初始化、前測條件、后執行體、循環體。
for (var i=0;i<10;i++){
//do something
}
for循環是javascript最常用的循環結構,直觀的代碼封裝風格被開發者喜愛。
2. while循環。while循環是最簡單的前測循環,由一個前測條件和一個循環體構成。
3. do-while循環是javascript唯一一種后測循環,由一個循環體和一個后測條件組成,至少會執行一次。
4. for-in循環。可以枚舉任何對象的屬性名。
循環的性能
javascript提供的四種循環類型中,只有for-in循環比其他幾種明顯要慢。因為每次迭代操作會同時搜索實例或原型屬性,for-in循環的每次迭代都會產生更多開銷。速度只有其他類型循環的七分之一。除非你明確需要迭代一個屬性數量未知的對象,否則應該避免使用for-in循環。如果你需要遍歷一個數量有限的已知屬性列表,使用其他循環類型會更快,比如數組。
除for-in外,其他循環類型的性能都差不多,類型的選擇應該基于需求而不是性能。
提高循環的性能
1. 減少每次迭代處理的事務
2. 減少迭代的次數
減少迭代的工作量
減少對象成員及數組項的查找次數。
在不影響的結果的情況下,可以使用倒序來略微提升性能。因為控制條件只要簡單的與零比較。控制條件與true比較時,任何非零數會自動轉換為true,而零值等同于false,實際上從兩次比較(迭代數少于總數么?是否為true?)減少到一次比較(它是true么)。當循環復雜度為O(n)時,減少每次迭代的工作量是最有效的方法。當復雜度大于O(n)時,建議著重減少迭代次數。
減少迭代次數
Duff's Device是一個循環體展開技術,使得一次迭代中實際上執行了多次迭代的操作。一個典型的實現如下:
//credit:Jeff Greenberg
var iterations = Math.floor(items.length / 8),
startAt = items.length/8,
i = 0;
do{
switch(startAt){
case 0: process(items[i++]);
case 7: process(items[i++]);
case 6: process(items[i++]);
case 5: process(items[i++]);
case 4: process(items[i++]);
case 3: process(items[i++]);
case 2: process(items[i++]);
case 1: process(items[i++]);
}
startAt = 0;
} while (--iterations);
Duff's Device背后的基本理念是:每次循環中最多可以調用8此process()。循環的迭代次數除以8。由于不是所有數字都能被8整除,變量startAt用來存放余數,表示第一次循環中應該調用多少次process()。
此算法稍快的版本取消了switch語句,并將余數處理和主循環分開
//credit:Jeff Greenberg
var i = items.length % 8;
while(i){
process(item[i--]);
}
i = Math.floor(items.length / 8);
while(i){
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
}
盡管這種實現方法用兩次循環代替之前的一次循環,但它移除了循環體中的switch語句,速度比原始循環更快。
如果循環迭代的次數小于1000,可能它與常規循環結構相比只有微不足道的性能提升。如果迭代數超過1000,那么執行效率將明顯提升。例如在500000此迭代中,其運行時間比常規循環減少70%
基于函數的迭代
ECMA-262第四版加入的數組方法:forEach()方法。此方法遍歷一個數組的所有成員,并在每個成員上執行一個函數。要運行的函數作為參數傳給forEach(),并在調用時接受三個參數,分別是當前的值、索引以及數組本身。盡管基于函數的迭代提供了一個更為便利的迭代方法,但它仍比基于循環的迭代要慢一些。對每個數組項調用外部方法所帶來的開銷是速度慢的主要原因。
條件語句
if-else對比switch
條件數數量越大,越傾向于使用switch,主要是因為易讀性。事實證明,大多數情況下switch比if-else運行得要快,但只有條件數量很大時才快得明顯。
優化if-else
最小化到達正確分支前所需要判斷的條件數量。最簡單的優化方法是確保最可能出線的條件放在首位。if-else中的條件語句應該總是按照從最大概率到最小概率的順序排列,以確保運行速度最快。假設均勻分部,可使用二分法的思想,重寫為一系列嵌套的if-else語句。
查找表
有些時候優化條件語句的最佳方案是避免使用if-else和switch。可以使用數組和普通對象來構建查找表,通過查找表訪問數據比用if-else或switch快很多。當單個鍵值存在邏輯映射時,構建查找表的優勢就能體現出來。(比如把按照順序的鍵值映射放到數組里)
遞歸
使用遞歸可以把復雜的算法變的簡單。潛在問題是終止條件不明確或缺少終止條件會導致函數長時間運行,并使得用戶界面處于假死狀態和瀏覽器的調用棧大小限制。
調用棧限制
javascript引擎支持的遞歸數量與javascript調用棧大小直接相關。
遞歸模式
當你遇到調用棧大小限制時,第一步應該檢查代碼中的遞歸實例。有兩種遞歸模式,第一種是調用自身,很容易定位錯誤。第二種是互相調用,很難定位。
迭代
任何遞歸能實現的算法同樣可以使用迭代來實現。使用優化后的循環代替長時間運行的遞歸函數可以提升性能,因為運行一個循環比反復調用一個函數的開銷要少的多。
歸并排序算法是最常見的用遞歸實現的算法:
function merge(left, right) {
var result = [];
while (left.length > 0 && right.length > 0){
if (left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
return result.concat(left).concat(right);
}
function mergeSort(items){
if (items.length == 1){
return items;
}
var middle = Math.floor(items.length / 2),
left = items.slice(0, middle),
right = items.slice(middle);
return merge(mergeSort(left),mergeSort(right));
}
使用迭代實現歸并算法:
//使用和上面相同的merge函數
function mergeSort(items){
if (items.length == 1){
return items;
}
var work = [];
for (var i=0, len=items.length;i < len; i++){
work.push([items[i]]);
}
work.push([]);
for (var lim=len; lim>1; lim = (lim+1)/2){
for (var j=0,k=0; k < lim; j++, k+=2){
work[j] = merge(work[k],work[k+1]);
}
work[j] = [];
}
return work[0];
}
盡管迭代版本的歸并排序算法比遞歸實現得要慢一些,但它不會像遞歸版本那樣受到調用棧限制的影響。把遞歸算法改用迭代實現是避免棧溢出錯誤的方法之一
Memoization
Memoization是一種避免重復工作的方法,它緩存前一個計算結果供后續計算使用,避免了重復工作。
使用Memoization技術來重寫階乘函數:
function memfactorial(n){
if(!memfactorial.cache){
memfactorial.cache={
"0":1,
"1":1
};
}
if(!memfactorial.cache.hasOwnProperty(n)){
memfactorial.cache[n] = n * memfactorial (n-1);
}
return memfactorial.cache[n];
}
字符串和正則表達式
字符串鏈接
+和+=
不應在等號右邊進行和被賦值的量無關的字符串拼接運算,這樣會創造臨時字符串。
例如:
str += "one" + "two";
會經歷四個步驟:
1. 在內存中創建一個臨時字符串
2. 連接后的字符串“onetwo”被賦值給該臨時字符串
3. 臨時字符串與str當前的值連接
4. 結果賦值給str
使用這種方式來代替:
str = str + "one" + "two";
//等價于 str = ((str + "one") + "two")
賦值表達式由str開始作為基礎,每次給它附加一個字符串,由做到右一次連接,因此避免了使用臨時字符串。
數組項合并
Array.prototype.join方法將數組的所有元素合并成一個字符串,它接受一個字符串參數作為分隔符插入每個元素的中間。大多數瀏覽器中,數組項合并比其他字符串連接的方法更慢。
String.prototype.concat
字符串的原生方法concat能接收任意數量的參數,并將每一個參數附加到所調用的字符串上。這是最靈活的字符串合并方法。多數情況下,使用concat比使用簡單的+和+=稍慢。
正則表達式優化
部分匹配比完全不匹配所用的時間要長。
正則表達式工作原理
1. 第一步編譯
瀏覽器會驗證正則表達式,然后把它轉換為一個原生代碼程序,用于執行匹配工 作。如果把正則對象賦值給一個變量,可以避免重復這一步。
2. 第二步設置起始位置
3. 第三步匹配每個正則表達式字元
4. 第四步匹配成功或失敗
回溯
當正則比到達時匹配目標字符串時,從左到右逐個測試表達式的組成部分,看是否能找到匹配項。在遇到量詞和分支時,需要決策下一步如何處理。如果遇到量詞,正則表達式需決定何時嘗試匹配更多字符;如果遇到分支,那么必須從可選項中選擇一個嘗試匹配。每當正則表達式做類似的決定時,如果有必要的話,都會記錄其他選擇,以備返回時使用。如果當前選項匹配成功,正則表達式繼續掃描表達式,如果其他部分也匹配成功,尼瑪匹配結束。但是如果當前選項找不到匹配值,或后面的部分匹配失敗,那么正則表達式會回溯到最后一個決策點,然后在剩余的選項中選擇一個。這個過程會一直進行,知道找到匹配項,或者正則表達式中量詞和分支選項的所有排列組合都嘗試失敗,那么它將放棄匹配從而移動到字符串的下一個字符,再重復此過程。
重復和回溯
貪婪匹配是段尾一個個回溯接下來的匹配內容,惰性正好相反;
回調失控
<b>最佳實踐:如果你的正則表達式包含了多個捕獲組,那么你需要使用適當的反向引用次數。</b>
嵌套量詞與回溯失控
所謂的嵌套量詞需要格外的關注且小心使用,以確保不會引發潛在的回溯失控。嵌套兩次是指兩次出線在一個自身被重復量詞修飾的組中。確保正則表達式的兩個部分不能對字符串的相同部分進行匹配
更多提高正則表達式效率的方法
1. 關于如何讓正則匹配更快失敗
正則表達式慢的原因通常是匹配失敗的過程慢。
2. 正則表達式以簡單、必需的字元開始
一個正則表達式的起始標記應當盡可能快速的測試并排除明顯不匹配的位置。盡量以一個錨、特定字符串、字符類和單詞邊界開始,盡量避免以分組或選擇字元開頭,避免頂層分支。
3. 使用量詞模式,使它們后面的字元互斥
當字符與字元相鄰或子表達式能夠重疊匹配時,正則表達式嘗試拆解文本的路徑數量將增加。
4. 減少分支數量,縮小分支范圍
分支使用豎線|可能要求在字符串的每一個位置上測試所有的分支選項。你通常可以通過使用字符集和選項組件來減少對分支的需求,或將分支在正則表達式上的位置推后。
5. 使用非捕獲組
捕獲組消耗時間和內存來記錄反向引用,并使它保持最新。如果你不需要一個反向引用,可以使用非捕獲組來避免這些開銷。
6. 只捕獲感興趣的文本以減少后處理
如果需要引用匹配的一部分,應該才去一切手段捕獲那些片段,再使用反向引用來處理。
7. 暴露必需的字元
嘗試讓正則表達式引擎更容易判斷哪些字元是必需的。
8. 使用合適的量詞
9. 把正則表達式賦值給變量并重用它們
避免在循環體中重復編譯正則表達式。
10. 將復雜的正則表達式拆分為簡單的片段
何時不使用正則表達式
當只是搜索字面字符串,尤其是事先知道字符串的哪一部分將要被查找時。正則表達式無法直接跳到字符串末尾而不考慮沿途的字符。
快速響應的用戶界面
瀏覽器UI線程
用于執行Javascript和更新用戶界面的進程通常被稱為“瀏覽器UI線程”。UI線程的工作基于一個見到那的隊列系統,任務會被保存到隊列中直到線程空閑。
瀏覽器限制
瀏覽器限制了javascript的運行時間。此類限制分為兩種:調用棧的大小限制和長時間運行腳本限制。
多久算太久
單個Javascript操作話費的總時間不應該超過100毫秒。
<b>最佳實踐:限制所有的Javascript任務在100毫秒或更短的時間內完成。</b>
使用定時器讓出時間片段
當Javascript不能在100毫秒或更短的時間內完成。最理想的方法是讓出UI線程的控制權,使得UI可以更新。
定時器基礎
在Javascript中可以使用setTimeout()和setInterval()創建定時器,它們接收相同的參數:要執行的函數和執行前的等待時間。定時器與UI線程的交互:定時器會告訴Javascript引擎先等待一定時間,然后添加一個Javascript任務到UI隊列。定時器代碼只有在創建它的函數執行完之后,才有可能執行。無論發生何種情況,創建一個定時器會造成UI線程暫停,如同它從一個任務切換到下一個任務。因此,定時器代碼會重置所有相關的瀏覽器限制,包括 長時間運行腳本定時器。此外,調用棧也會在定時器中重置為0。setTimeout()和setInterval()幾近相同,如果 UI隊列中已經存在由同一個setInterval()創建的任務,那么后續任務不會被添加到UI隊列中。如果setTimeout()中的函數需要消耗比定時器延時更長的運行時間,那么定時器代碼中的延時幾乎是不可見的。
定時器的精度
Javascript定時器延遲通常不太準確,相差大約為幾毫秒,無法用來精確計算時間。而且還存在最小值的限制。
使用定時器處理數組
是否可以用定時器取代循環的兩個決定性因素:處理過程是否必須同步;數據是否必須按照順序處理;如果兩個答案都是否,那么代碼適用于定時器分解任務。
var todo = items.concat();
// 克隆原數組
setTimeout(function(){
// 取得數組的下一個元素并進行處理
process(todo.shift());
// 如果還有需要處理的元素,創建另一個定時器
if(todo.length > 0){
setTimeout(arguments.callee, 25);
} else {
callback(items);
}
}, 25);
每個定時器的真實延時在很程度上取決于具體情況。普遍來講,最好使用至少25毫秒,因為再小的延時,對大多數UI更新來說不夠用。
記錄代碼運行使勁啊
通過定時器創建Date對象并比較它們的值來記錄代碼運行事件。加號可以將Date對象轉換成數字,那么在后續的運算中就無須轉換了。避免把任務分解成過于零碎的碎片,因為定時器之間有最小間隔,會導致出線空閑事件。
定時器與性能
當多個重復的定時器同時創建往往會出線性能問題。因為只有一個UI線程,而所有的定時器都在爭奪運行時間。那些間隔在1秒或1秒以上的低頻率的重復定時器幾乎不會影響Web應用的響應速度。這種情況下定時器延遲遠遠超過UI線程產生瓶頸的值,可以安全的重復使用。當過個定時器使用較高的頻率(100到200毫秒之間)時,會明顯影響性能。在web應用中限制高頻率重復定時器的數量,作為代替方案,使用一個獨立的重復定時器每次執行多個操作。
Web Worker
引入了一個接口,能使代碼運行并且不占用瀏覽器UI線程的時間。
Worker
沒有綁定UI線程,每個Web Worker都有自己的全局環境,其功能只是Javascript特性的一個子集。運行環境由如下部分組成:一個navigator對象,值包括四個屬性:appName、appVersion、userAgent和platform。
一個location對象(與window.location相同,不過所有屬性都是只讀的。)。
一個self對象,指向全局worker對象。
一個importScipt()方法,用來加載Worker所用到的外部javascript文件。
所有的ECMAScript對象
XMLHttpRequest構造器
setTimeout()方法和setInterval()方法
一個close()方法,它能立刻停止Worker運行
由于Web Worker有著不同的全局運行環境,因此你無法從javascript代碼中創建它。需要創建一個完全獨立的javascript文件,其中包含了需要在Worker中運行的代碼。要創建網頁人工線程,你必須傳入這個javascript文件的URL;
與Worker通信
通過事件接口進行通信。網頁代碼可以通過postMessage()方法給Worker傳遞數據,它接受一個參數,即需要傳遞給Worker的數據。此外,Worker還有一個用來接收信息的onmessage事件處理器。Worker可通過它自己的postMessage()方法把信息回傳給頁面。消息系統是網頁和Worker通信的唯一途徑。只有特定類型的數據可以使用postMessage()傳遞。你可以傳遞原始值(字符串、數字、布爾值、null和undefined),也可以傳遞Object和Array的實例,其他類型就不允許了。有效數據會被序列化,傳入或傳出Worker,然后反序列化。雖然看上去對象可以直接傳入,但對象實例完全是相同數據的獨立表述。
加載外部文件
Worker 通過importScript()方法加載外部javascript文件,該方法接收一個或多個javascript文件URL作為參數。importScript()的調用過程是阻塞式的,知道所有所有文件加載并執行完成之后,腳本才會繼續運行。由于Worker在UI線程之外運行,所以這種阻塞并不會影響UI響應。
Web Worker適合用于那些處理純數據,或者與瀏覽器UI無關的長時間運行腳本。盡管它看上去用處不大,但Web應用中通常有一些數據處理功能將收益于Worker而不是定時器。
可能的用處:
- 編碼/解碼大字符串
- 復雜數學運算
- 大數組排序
- 任何超過100毫秒的處理過程,都應當考慮Worker方案是不是比基于定時器的方案更為合適。
Ajax
Ajax是高性能javascript的基礎。它可以通過延遲下載體積較大的資源文件來使得頁面加載速度更快。它通過異步的方式在客戶端和服務端之間傳輸數據,避免同時傳輸大量數據。
數據傳輸
請求數據
有五種常用技術用于想服務器請求數據:
- XMLHttpRequest
- Dynamic script tag insertion(腳本動態注入)
- iframes
- Comet
- Multipart XHR
現代高性能Javascript中使用的三種技術是:XHR、動態腳本注入和Multipart XHR
XMLHttpRequest
XMLHttpRequest是目前最常用的技術,它允許異步發送和接收數據。由于XHR提供了高級的控制,所以瀏覽器對其增加了一些限制。你不能使用XHR從外域請求數據。對于那些不會改變服務器狀態,只會獲取數據(冪等行為)的請求,應該使用GET。經GET請求的數據會被緩存起來,如果需要多次請求統一數據的話,它會有助于提升性能。只有當請求的URL加上參數的長度接近或超過2048個字符時,才應該用POST獲取數據。因為IE限制URL長度,過長將導致請求的URL被截斷。
動態腳本注入
這種技術客服了XHR的最大限制:它能跨域請求數據。這是一個Hack,你不需要實例化一個專用對象,而可以使用javascript創建一個新的腳本標簽,并設置它的src屬性為不同域的URL。與XHR相比,動態腳本注入提供的控制是有限的。只能使用GET方法而不是POST方法。不能設置請求的超時處理或重試;不能訪問請求的頭部信息,不能把整個響應信息作為字符串來處理。因為響應消息作為腳本標簽的源碼,它必須是可執行的javascript代碼。你不能使用純XML、純JSOn或其他任何格式的數據,無論哪種格式,都必須封裝在一個回調函數中。這項技術的速度卻非常快。響應消息是作為javascript執行,而不是作為字符串需要進一步處理。正因如此,它有潛力成為客戶端獲取并解析數據最快的方法。
Multipart XHR
允許客戶端只用一個HTTP請求就可以從服務端向客戶端傳送多個字元。它通過在服務端將字元打包成一個由雙方約定的字符串分割的長字符串并發送到客戶端。然后用javascript代碼處理這個長字符串,并根據它的mime-type類型和傳入的其他“頭信息”解析出每個資源。缺點:資源不能被瀏覽器緩存。
能顯著提高性能的場景:
頁面包含了大量其他地方用不到的資源,尤其是圖片;
網站已經在每個頁面中使用了一個獨立打包的Javascript或CSS文件以減少http請求;
發送數據
XMLHttpRequest
當使用XHR發送數據到服務器時,GET方式會更快。這是因為,對少量數據而言一個GET請求只發送一個數據包。而一個POST請求至少要發兩個數據包,一個裝載頭信息,另一個裝載POST正文。POST更適合發送大量數據到服務器,因為它不關心額外數據包的數量,另一個原因是URL長度有限制,它不可能使用過長的GET請求。
Beacons
類似動態腳本注入。使用Javascript創建一個新的Image對象,并把src屬性設置為服務器上腳本的URL。該URL包含了我們要通過GET傳回的鍵值對數據。服務器會接受數據并保存下來,無須向客服端發送任何回饋信息,因此沒有圖片會實際顯示出來。這是回傳信息最有效的方式。性能消耗更小,而且服務器端的錯誤不影響客戶端。缺點:無法發送POST數據,而URL的長度有最大值,所以可以發送的數據的長度被限制的相當小。
數據格式
考慮數據格式時唯一需要比較的標準就是速度
XML
當Ajax最先開始流行時,它選擇了XML作為數據格式。優勢:極佳的通用性、格式嚴格,且易于驗證。缺點:冗長,依賴大量結構、有效數據的比例很低、語法模糊,如果有其他格式可選不要使用它。
JSON
是一種使用Javascript對象和數組直接量編寫的輕量級且易于解析的數據格式。
JSON-P
事實上,JSON可以被本地執行會導致幾個重要的性能影響。當使用XHR時,JSON數據被當成字符串返回。在使用動態腳本注入時,JSON數據要被當成另一個Javascript文件并作為原生代碼執行,為實現這一點必須封裝在一個回調函數中。JSON-P因為回調包裝的原因略微增大了文件尺寸,但性能提升巨大。由于數據是當作原生的Javascript,因此解析速度跟原生Javascript一樣快。最快的JSON格式是使用數組形式的JSON-P。不要把敏感數據編碼在JSON-P中,因為無法確認它是否保持著私有調用狀態。
HTML
通常你請求的數據需要被轉換成HTML以顯示到頁面上。Javascript可以較快地把一個較大的數據結構轉換成簡單的HTML,但在服務器處理會快很多。一種可考慮的技術是在服務器上構建好整個HTML再傳回客戶端,Javascript可以很方便地通過innerHTML屬性把它插入頁面相應的位置。取點:臃腫的數據格式、比XML更繁雜。在數據本身的最外層,可以嵌套HTML標簽,每個都帶有id、class和其他屬性。HTML格式可能比實際數據占用更多空間。應當在客戶端的瓶頸是CPU而不是帶寬時才使用此技術。
自定義格式
理想的數據格式應該只包含必要的結構,以便你可以分解出每個獨立的字段。最重要的決定就是采用哪種分隔符,它應當是一個單字符,而且不應該存在你的數據中。
Ajax性能指南
緩存數據
在服務端,設置HTTP頭信息以確保你的響應會被瀏覽器緩存。
在客戶端,把獲取到的信息存儲到本地,從而避免再次請求。
設置HTTP頭信息
如果希望ajax能被瀏覽器緩存,那么你必須使用GET方式發送請求并且需要在響應中發送正確的HTTP頭信息。Expires頭信息會告訴瀏覽器應該緩存多久。它的值是一個日期。
本地數據存儲
直接把從服務器接收到的數據儲存起來。可以把響應文本保存到一個對象中,以URL為鍵值作為索引。
Ajax類庫的局限性
所有的Javascript類庫都允許你訪問一個Ajax對象,它屏蔽了瀏覽器之間的差異,給你一個統一的接口。為了統一接口的功能,類庫簡化接口,使得你不能訪問XMLHttpRequest的完整功能。
編程實踐
避免雙重求值
Javascript允許你在程序中提取一個包含代碼的字符串,然后動態執行它。有四種標準方法可以實現:eval()、Function()構造函數、setTimeout()和setInterval()。首先會以正常的方式求值,然后在執行的過程中對包含于字符串的代碼發起另一個求值運算。每次使用這些方法都要創建一個新的解釋器/編譯器實例,導致消耗時間大大增加。
大多數時候沒有必要使用eval()和Function(),因此最好避免使用它們。定時器則建議傳入函數而不是字符串作為第一個參數。
使用Object/Array直接量
Javascript中創建對象和數組的方法有多種,但使用對象和數組直接量是最快的方式。
避免重復工作
別做無關緊要的工作,別重復做已經完成的工作。
延遲加載
第一次被調用時,會先檢查并決定使用哪種方法去綁定或取消綁定事件處理器。然后原始函數被包含正確操作的新函數覆蓋。最后一步調用新的函數,并傳入原始參數。隨后每次調用都不會再做檢測,因為檢測代碼已經被新的函數覆蓋。調用延遲加載函數時,第一次總會消耗較長的費時間,因為它必須運行檢測接著再調用另一個函數完成任務。但隨后調用相同的函數會更快,因為不需要再執行檢測邏輯。當一個函數在頁面中不會立刻調用時,延遲加載是最好的選擇。
條件預加載
它會在腳本加載期間提前檢測,而不會等到函數被調用。檢測的操作依然只有一次,知識它在過程中來的更早。條件預加載確保所有函數調用消耗的時間相同。其代價是需要在腳本加載時就檢測,而不是加載后。預加載適用于一個函數馬上就要被用到,并且在整個頁面的生命周期中頻繁出現的場合。
使用快的部分
<b>運行速度慢的部分實際上是代碼,引擎通常是處理過程中最快的部分。</b>
位操作
使用位運算代替純數學操作:對2的取模運算可以被&1代替,速度提高很多。位掩碼:處理同時存在多個布爾選項時的情形,思路是使用單個數字的每一位來判定是否選項成立,從而有效得把數字轉換為布爾值標記組成的數組。
原生方法
原生方法更快,因為寫代碼前就存在瀏覽器中了,并且都是用底層語言比如c++編寫的。經驗不足的Javascript開發者經常犯的錯誤就是在代碼中進行復雜的數學運算,而沒有使用內置的Math對象中那些性能更好的版本。另一個例子是選擇器API,它允許使用CSS選擇器來查找DOM節點。原生的querySelector()和querySelectorAll()方法完成任務平均所需時間是基于Javascript的CSS查詢的10%。
構建并部署高性能Javascript應用
<b>合并多個Javascript文件,網站提速指南中第一條也是最重要的一條規則,就是減少http請求數。</b>
預處理Javascript文件
預處理你的Javascript源文件并不會讓應用變的更快,但它允許你做些其他的事情,例如有條件地插入測試代碼,來衡量你的應用程序的性能。
Javascript壓縮
指的是把Javascript文件中所有與運行無關的部分進行剝離的過程。剝離的內容包括注釋和不必要的空白字符。該過程通常可以將文件大小減半,促使文件更快被下載,并鼓勵程序員編寫更好的更詳細的注釋。
構建時處理對比運行時處理
普遍規則是只要能在構建時完成的工作,就不要留到運行時去做。
Javascript的http壓縮
當Web瀏覽器請求一個資源時,它通常會發送一個Accept-Encoding HTTP頭來告訴Web服務器它支持哪種編碼轉換類型。這個信息主要用來壓縮文檔以更快的下載,從而改善用戶體驗。Accept-Encoding可用的值包括:gzip、compress、deflate和identity。gzip是目前最流行的編碼方式。它通常能減少70%的下載量,成為提升Web應用性能的首選武器。記住Gzip壓縮主要適用于文本,包括Javascript文件。
緩存Javascript文件
緩存HTTP組件能極大提高網站回訪用戶的體驗。Web服務器通過Expires HTTP響應頭來告訴客戶端一個字元應當緩存多長事件。它的值是一個遵循RFC1123標準的絕對時間戳。
處理緩存問題
適當的緩存控制能提升用戶體驗,但它有一個缺點:當應用升級時,你需要確保用戶下載到最新的靜態內容。這個問題可以通過把改動過的靜態資源重命名來解決。
使用內容分發網絡(CDN)
內容分發網絡是在互聯網上按地理位置設置分部計算機網絡,它負責傳遞內容給終端用戶。使用CDN的主要原因是增強Web應用的可靠性、可擴展性,更重要的是提升性能。事實上,通過向地理位置最近的用戶輸入內容,CDN能極大減少網絡延時。