大多數瀏覽器都是用單一進程處理UI界面的刷新和JavaScript的腳本執行,因此同一時間只能作一件事,Javascript執行過程耗時越久,瀏覽器等待響應的時間就越長。javascript
因此,HTML頁面在遇到php
儘管減小Javascript文件的大小並限制HTTP請求次數僅僅只是第一步,下載單個較大的Javascript腳本執行也許要鎖死大量的事件,因此無阻塞的腳本的意義在於頁面加載完成以後再下載腳本。css
<script defer>
這是告知,延遲腳本內的內容不會更改DOM,只有IE 4+和Firefox 3.5+瀏覽器支持。html
defer意味着腳本會先下載,但只有到DOM加載完成以前纔會執行,不與頁面的其餘資源衝突,可並行。java
這種方法的優勢是,能夠下載Javascript代碼但不當即執行,並且幾乎適用全部主流瀏覽器。node
侷限性在於Javascript文件必須與所請求的頁面處於相同的域,因此Javascript文件不能從CDN下載。web
向頁面中添加大量的JavaScript的推薦作法只需兩步:先添加動態加載所須要的代碼,再加載初始化頁面所須要的代碼。ajax
前者代碼精簡,執行很快。正則表達式
<script type="text/javascript"> function loadScript(url, callback) { var script = document.createElement("script"); script.type = "text/javascript"; if (script.readyState) { script.onreadstatechange = function() { if (script.readyState == "loaded" || script.readyState = "complete") { script.onreadystatechange = null; callback(); } } } else { script.onload = function() { callback() } } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); } loadScript("the-rest.js", function() { Application.init(); }); </script>
數據的存儲位置不一樣,代碼執行時的數據檢索速度也不一樣。算法
對於Javascript來講,有下面四種基礎的數據存取位置。
字面量
字面量只表明自身,不存儲在特定位置。JS中的字面量有:字符串、數字、布爾值、對象、數組、函數、正則表達式,以及特殊的null和undefined值。
本地變量
開發人員使用關鍵字var定義的數據存儲單元。
數組元素
存儲在JS數組對象內部,以數字做爲索引。
對象成員
存儲在JS對象內部,以字符串做爲索引。
做用域概念對於理解Javascript相當重要,不只在性能方面,還在功能方面。
每一個函數都是Function的實例,Function對象與其餘對象同樣,擁有能夠編寫的屬性以及,一系列只供JavaScript引擎存儲的內部屬性,其中一個是[[scope]]。
[[scope]]包含了一個函數被建立的做用域中對象的集合。
在函數執行過程當中,每遇到一個變量,都會經歷一次標識符解析過程以及從哪裏獲取存儲數據。若是當前執行環境(做用域頭)找不到該變量,就搜索下一個做用域,若是都找不到則爲undefined。
標識符所在的位置越深,讀寫速度也就越慢。因此,讀取局部變量時最快的,而讀取全局變量時最慢的。
對此有個經驗法則:若是某個跨做用域的值在函數中被引用一次以上,那麼就把它存儲到局部變量裏。
通常來講,一個執行環境的做用域鏈是不會改變的。可是,有兩個語句能夠在執行時臨時改變做用域鏈。
一個是with語句,另外一個是try-catch語句。
with(context),with語句有一個問題,那就是with()裏的參數做爲做用域鏈頭後,函數的局部變量都會變成第二個做用域鏈對象中,這樣每次訪問都得訪問兩次。
try-catch的catch子句也有這種效果,它把一個異常對象推到做用域鏈首部,可是子句執行完畢,做用域鏈就會返回以前的狀態。
with、try-catch和eval()都是動態做用域。
閉包是JS最強大的特性之一,它容許函數訪問局部做用於以外的數據。
一般執行環境一銷燬,活動對象也應該銷燬,可是由於閉包中,對活動對象的引用依舊存在,因此活動對象並不會被銷燬,所以也須要更高的內存開銷。
JS對象基於原型,實例屬性proto指向原型對象且只對開發者可見。
hasOwnProperty()區分原型屬性和實例屬性。
很少解釋。
不太常見的寫法:window.location.href,嵌套成員會致使JS引擎搜索全部對象成員。嵌套得越深,讀取速度就會越慢。
很少解釋
在JS中,數據存儲的位置會對代碼總體性能產生重大的影響。數據存儲有4種方法:字面量、變量、數組項和對象成員。它們有着各自的性能特色。
首先必須先明確一點:用腳本進行DOM操做的代價很是昂貴。
DOM至關於瀏覽器HTML文檔,XML文檔與JS的程序接口(API),與語言無關。因此DOM與JS之間的交流消耗費用也就越高。
通用的經驗法則:減小訪問DOM的次數,把運算儘可能留在ECMAScript這端來處理。
innerHTML非標準可是支持性良好,在老瀏覽器中innerHTML比DOM更加高效,可是innerHTML最好與數組結合起來使用。這樣效率會更高。
element.cloneNode()
方法克隆節點。
HTML集合是包含了DOM節點引用的類數組對象。如下方法的返回值就是一個集合。
類數組對象沒有push.slice等方法,可是有length屬性且可遍歷。
HTML集合與文檔時刻保持鏈接,因此須要最新的消息須要時刻查詢。
在循環語句中讀取數組的length是不推薦的作法,最好是把數組的長度存儲在一個局部變量中。
第一優化原則是把集合存儲在局部變量中,並把length緩存在循環外部,而後用局部變量替代這些須要屢次讀取的元素。
舉個例子:
// 較慢 function collectionGlobal() { var coll = document.getElementsByTagName('div'), len = coll.length, name = ''; for (var count = 0; count < len; count++) { name = document.getElementsByTagName('div')[count].nodeName; name = document.getElementsByTagName('div')[count].nodeType } } // 很快 function collectionGlobal() { var coll = document.getElementsByTagName('div'), len = coll.length, name = '', el = null; for (var count = 0; count < len; count++) { el = coll[count]; name = el.nodeName; name = el.nodeType } }
一般你須要從某一個DOM元素開始,操做周圍的元素,或者遞歸查找全部子節點。你可使用childNodes獲得元素集合,或者用nextSibling來獲取每一個相鄰元素。
DOM元素屬性諸如childNodes,firstChild和nextSibling並不區分元素節點和其餘類型節點,若是要過濾的話,實際上是沒必要要的DOM操做。
如今能區分元素節點和其餘節點的DOM屬性以下:
屬性名 | 被替代的屬性 |
---|---|
children | childNodes |
childElementCount | childNodes.length |
firstElementChild | firstChild |
lastElementChild | lastChild |
nextElementSibling | nextSibling |
previousElementSibling | previousSibling |
children屬性的支持率較高。
document.querySelectorAll(‘#menu a’);
document.querySelector(」) 選擇匹配的第一個節點
瀏覽器下載完頁面中的全部組件,以後會解析並生成兩個內部數據結構:
DOM樹
表示頁面結構
渲染樹
表示DOM節點如何顯示
DOM樹中全部須要顯示的節點在渲染樹中至少存在一個對應的節點(隱藏的DOM元素在渲染樹中沒有對應的節點)。
一旦DOM樹與渲染樹構建完成,瀏覽器就開始繪製頁面元素。
當DOM的變化影響了元素的幾何屬性(寬和高)——好比改變邊框寬度或給段落增長文字,致使行數增長——瀏覽器須要從新計算元素的幾何屬性,一樣其餘元素的集合屬性位置也會所以受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並從新構造渲染樹,這個過程被稱爲重排。
完成重排後,瀏覽器會從新繪製受影響的部分到屏幕中,這個過程被稱爲重繪。有些樣式的改變,好比背景的變化不影響到佈局,因此只會重繪。
因爲每次重排都會產生計算消耗,大多數瀏覽器經過隊列化修改並批量執行來優化重排過程。然而,你可能會不自以爲強制刷新隊列並要求計劃任務馬上執行。獲取佈局信息的操做會致使列隊刷新,好比:
- offsetTop,offsetLeft…
- scrollTop,scrollLeft…
- clientTop,clientLeft…
- getComputedStyle()
由於以上方法要求返回最新的佈局信息,因此瀏覽器不得不把「待處理變化」觸發重排。
el.style.cssText = "";
修改樣式信息。el.className = "";
當你須要對DOM元素進行一系列操做時,能夠經過如下步驟來減小重繪和重排的次數:
1.使元素脫離文檔流
2.對其應用多重改變
3.把元素帶回文檔
有三種方法可使DOM脫離文檔:
- 隱藏元素,應用修改,從新顯示
- 使用文檔片斷在當前DOM以外構建一個子樹,再把它拷貝迴文檔
- 將原始元素拷貝到一個脫離文檔的節點中,修改副本,完成後再替換原始元素
舉個例子:
<ul id="mylist"> <li><a></a></li> <li><a></a></li> <li><a></a></li> </ul> // 假設要將更多的數據插入到這個列表中 var data = [ { "name": "Nicholas", "url": "...." } ] // 用來更新指定節點數據的通用函數 function appendDataToElement(appendToElement, data) { var a, li; for (var i = 0, max = data.length; i < max; i++) { a = document.createElement('a'); a.href = data[i].url; a.appendChild(document.createTextNode(data[i].name)); li = document.createElement('li'); li.appendChild(a); appendToElement.appendChild(li); } } // 不考慮重排 var ul = document.getElementById('mylist'); appendDataToElement(ul, data); // 第一種方法 var ul = document.getElementById('mylist'); ul.style.display = 'none'; appendDataToElement(ul, data); ul.style.display = 'block'; // 第二種方法(推薦) var fragment = document.createDocumentFragment(); appendDataToElement(fragment, data); document.getElementById('mylist').appendChild(fragment); // 第三種方法 var old = document.getElementById('mylist'); var clone = old.cloneNode(true); appendDataToElement(clone, data); old.parentNode.replaceChild(clone, old);
對於須要操做佈局信息的地方,最好用一個局部變量來緩存,否則查詢一次就會刷新一次渲染隊列並應用全部變動。
通常而言動畫的展開與隱藏會影響大量元素的重排,使用一下步驟能夠避免頁面中大部分重排。
儘可能避免使用hover。
綁定的事件越多,代價也越大,要麼加劇了頁面負擔,要麼是增長了運行期的執行時間。
在父元素綁定個事件,利用冒泡。
訪問和操做DOM是現代Web應用的重要部分。但每次穿越鏈接ECMAScript和DOM兩個島嶼之間的橋樑,都會被收取「過橋費」。
- 最小化DOM訪問次數,儘量在JavaScript端處理。
- 若是須要屢次訪問某個DOM節點,請使用局部變量存儲它的引用。
- 當心處理HTML集合,由於它實時鏈接着底層文檔。把集合的長度緩存到一個變量中,並在迭代中使用它,若是須要常常操做集合,建議把它拷貝到一個數組中。
- 若是可能的話,使用速度更快的API
- 要留意重繪和重排,批量修改樣式時,「離線」操做DOM樹,使用緩存,減小訪問佈局信息的次數
- 動畫中使用絕對定位,使用拖放代理
- 使用事件委託來減小事件處理器的數量
代碼的數量不是影響代碼運行速度的必然因素,代碼的組織結構和解決具體問題的思路纔是。
大多數編程語言中,代碼執行時間都消耗在循環中。
for循環
while循環
do-while循環
for-in循環
不斷引起循環性能爭論的源頭是循環類型的選擇。在JS提供的四種循環類型中,只有for-in明顯比其餘的慢。
優化循環的第一步就是減小對象成員和數組項的查詢次數。好比把數組長度賦值給一個局部變量。
第二步是顛倒數組的順序,一樣能夠提高循環性能。
第三步是減小迭代次數,達夫設備。
達夫設備其實是把一次迭代操做展開成屢次迭代操做。
思路是,每次循環中最多可調用8次process(),若是有餘數,則表示第一次process()執行幾回。
var iterations = Math.floor(items.length / 8), startAt = items.length % 8, i = 0; do { switch(startAt) { case 0: process(items[i++]); ... ... case 8: process(items[i++]); } startAt = 0; } while (--iterations);
達夫設備對於1000次以上的循環有很大的提高。
Array.forEach(function(value, index, array){ process(value); })
條件數量少時用if-else,多時用switch。
目標:最小化到達正確分以前所需判斷的條件數量。
最簡單的優化方法是確保最可能出現的條件放在首位。
還有一種是增長if-else的嵌套,儘量減小判斷次數。
當有大量離散值須要測試時,或者條件語句數量很大時,JS能夠經過數組和普通對象來構建查找表,速度要快得多。
switch(value) { case 0: return result0; case 1: return result1; } var results = [results0, results1] return results[index]
階乘就是用遞歸實現的,可是遞歸的問題在於終止條件不明確或缺乏終止條件會致使函數長時間運行,並且可能會遇到瀏覽器的「調用棧大小限制」。
瀏覽器有棧限制。
有兩種遞歸模式。直接遞歸和隱伏模式。
任何遞歸能實現的算法一樣能夠用迭代來實現。
把計算結果緩存,運行前先判斷。
functiom 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]; }
運行的代碼量數量越大,使用這些策略所帶來的性能提高也就越明顯。
UI線程把一個個JS或者UI渲染任務放到隊列中逐個執行,最理想的狀況就是隊列爲空,這樣任務能夠即刻執行。
瀏覽器對JS任務的執行時間有限制,從棧大小限制和運行時間兩方面來限制。
「若是JS運行了整整幾秒鐘,那麼極可能是你作錯了什麼….」
單個JS文件的操做總時間不能超過100毫秒。
若是100毫秒內不能解決JS任務,那麼就把線程讓出來執行UI渲染。
setTimeout()和setInterval()會告訴JS引擎等待一段時間,而後添加一個JS任務到UI隊列。
定時器不可用於測量實際時間,有幾毫秒的誤差。
若是第四章的循環優化仍是沒有將JS任務縮短到100毫秒之內,那麼下一步的優化步驟就是定時器。
能夠的話,將一個大任務分割成無數小任務。
不超過50毫秒的JS任務是很是好的用戶體驗,可是有時候一次性只執行一個任務,這樣執行效率反而不高。
多個重複的定時器同時建立每每會出現性能問題。間隔在1s或者1s以上的重複定時器不會影響Web應用得響應速度。
Web Workers是HTML5最初的一部分,它的出現意味着JS任務能夠單獨分離出去而不佔用瀏覽器的UI渲染。
Web Workers不能處理UI進程,這就意味着它不能接觸不少瀏覽器的資源。
Web Workers的運行環境由以下部分組成:
- navigator對象,包括appName、appVersion、user Agent和platform。
- 一個location對象,(與window.location同,可是隻讀)
- 一個self對象,指向全局worker對象。
- 一個importScripts()方法,用來加載Worker所用到外部JS文件
- 全部的ECMAScript對象
- XMLHttpRequest構造器
- setTimeout()和setInterval()方法
-一個close()方法,它能馬上中止Worker運行
var worker = new Worker("code.js")
Worker與網頁代碼經過事件接口進行通訊,網頁代碼經過postMessage()方法給Worker傳遞數據,它接收一個參數,即須要傳給Worker的數據。此外,Worker還有一個用來接收信息的onmessage事件處理器。
var worker = new Worker('code.js') worker.onmessage = function(event) { process(event.data) } worker.postMessage('data') //code.js self.onmessage = function(event) { self.postMessage("Hello" + event.data) }
importScripts()阻塞
var worker = new Worker("jsonparser.js"); worker.onmessage = function(event) { var jsonData = event.data evaluateData(jsonData) } worker.postMessage(jsonText) self.onmessage = function(event) { var jsonText = event.data var jsonData = JSON.parse(jsonText) self.postMessage(jsonData) }
Ajax能夠經過延遲和異步加載大資源。
Ajax從最基本的層面來講,是一種與服務器通訊而無需重載頁面的方法,數據能夠從服務器獲取或發送給服務器。
有五種經常使用技術用於向服務器請求數據:
- XMLHttpRequest(XHR)
- Dynamic script tag insertion 動態腳本注入
- iframes
- Comet
- Multipart XHR
XHR
var url = '/data.php'; var params = [ 'id=934875', 'limit=20' ]; var req = new XMLHttpRequest(); req.onreadystatechange = function() { if (req.readystate === 4) { } } req.open('get', url + '?' + params.join('&'), true) req.setRequestHeader('X-Requested-with', 'XMLHttpRequst'); req.send(null)
XHR的get請求是冪等行爲,即一次請求和屢次請求並不會有反作用。
動態腳本注入
這個與XHR不一樣的地方在於它不用在乎跨域問題。
var scriptElement = document.createElement('script'); scriptElement.src = ''; document.getElementsByTagName('head')[0].appendChild(scriptElement)
不能設置頭信息,參數傳遞也只能用GET,不能設置請求的超時處理。
由於響應消息做爲腳本標籤的源碼,它必須是可執行的JS代碼。
Multipart XHR
MXHR容許客戶端只用一個HTTP請求就能夠從服務端向客戶端傳送多個資源。它經過在服務端將資源打包成一個由雙方約定的字符串分割的長字符串併發送到客戶端,而後用JS代碼處理這個長字符串,並根據它的mime-type類型和傳入的其餘「頭信息」解析出每一個資源。
惟一須要比較的標準就是速度。
須要解析結構,才能讀取值。
JSON-P JSON填充,在動態腳本注入時必須放在回調函數裏,否則就會被當作另一個JS文件執行(因此也要尤爲注意腳本攻擊。)
最快的Ajax請求就是沒有請求。有兩種主要的方法能夠避免發送沒必要要的請求:
- 在服務端,設置HTTP頭信息以確保響應會被瀏覽器緩存
- 在客戶端,把獲取到的信息存儲到本地,從而避免再次請求。
設置HTTP頭信息
若是你但願Ajax響應可以被瀏覽器緩存,那麼你必須使用GET方式發出請求。但這還不夠,你還必須在相應中發送正確的HTTP頭信息。
Expires頭信息會告訴瀏覽器應該緩存響應多久,它的值是一個日期,過時以後,對該URL的任何請求都再也不從緩存中獲取,而是會從新訪問服務器。
本地數據存儲
能夠把響應文本保存到一個對象中,以URL爲鍵值作索引。
var localCache = {}; function xhrRequest(url, callback) { // 檢查此URL的本地緩存 if (localCache[url]) { callback.success(localCache[url]); return; } // 此URL對應的緩存沒有找到,則發送請求 var req = createXhrObject(); req.onerror = function() { callback.error(); } req.onreadystatechange = function() { ... localCache[url] = req.responseText; callback.success(req.responseText) } req.open("GET", url, true) req.send(null) }
瀏覽器之間有些差別,不過大多數類庫都有封裝。
JS和其餘不少腳本語言同樣,容許你在程序中提取一個包含代碼的字符串,而後動態執行。
有四種標準方法能夠實現:eval(),Function(),setTimeout(),setInterval()。
在JS代碼中執行另一段JS代碼就會形成雙重求值,因此eval和Function不推薦使用,setTimeout和setInterval的第一個參數最好是回調函數。
使用直接量建立Object/Array
當一個函數在頁面中不會被當即調用時,延遲加載時最好的選擇,即若是須要針對不一樣的瀏覽器寫不一樣的代碼,在第一次就判斷用的是哪一種,而後在內部重寫函數。
var addHandler = document.body.addEventListener ? function1 : function2