在上一節推薦實踐中其實不少方面是與效率有關的,但那些都是語言層次的優化,這一節偏重學習大的方面的優化,好比JavaScript腳本的組織,加載,壓縮等等。 固然在此以前,分析一下瀏覽器的特徵仍是頗有意義的。javascript
瀏覽器的特徵
1. 瀏覽器的渲染過程
在詳細討論腳本文件的優化前,咱們先來看一下瀏覽器是如何渲染一個HTML頁面的。
當瀏覽器渲染一個HTML頁面的時候,它老是從頁面的開始位置按順序向頁面末尾依次渲染,當頁面遇到引用外部文件的時候(JavaScript腳本,CSS文件等),頁面渲染就會停下來等待該外部文件下載完並執行完成,而後繼續向下渲染。
瀏覽器在下載和執行腳本時出現阻塞的緣由在於,腳本可能會改變頁面或相關代碼,這些操做對後面頁面內容形成影響。因而當瀏覽器遇到<script>標籤時,因爲當前HTML頁面無從獲知該JavaScript代碼是否會向頁面添加內容,或引入其餘元素,或甚至移除某些標籤元素,所以,這時瀏覽器會中止處理頁面,先執行JavaScript代碼,而後再繼續解析和渲染頁面。
一樣的狀況也發生在使用src屬性加載JavaScript的過程當中,瀏覽器必須先花時間下載外鏈文件中的代碼,而後解析並執行它。JavaScript執行過程耗時越久,瀏覽器等待響應用戶輸入的時間就越長。在這個過程當中,頁面渲染和用戶交互徹底被阻塞了。css
2. 多線程的瀏覽器
在上面的過程當中,至少使用到了瀏覽器的HTTP申請線程,界面渲染引擎線程(大多數瀏覽器JavaScript解釋引擎也包含在這個線程中,這個也解釋了爲何執行JavaScript的時候會阻塞界面渲染),瀏覽器事件管理線程,這些線程都是由瀏覽器建立和管理的,這些線程執行特定的功能(從名稱上就能看出來)並互相協做,完成頁面的申請,資源的下載,頁面的渲染,事件的處理等行爲。html
3. 單線程的JavaScript解釋引擎
我們再把目光聚焦在JavaScript解釋引擎上,毫無疑問,JavaScript腳本就是由它解釋執行的。
當咱們執行一個行爲的時候,這個行爲的功能一般都是由JavaScript引擎和瀏覽器其它線程協做完成的。
JavaScript解釋引擎是單線程的,每一個window(或者說一個頁面吧)一個JS線程,既然是單線程的,在某個特定的時刻只有特定的代碼可以被執行,並阻塞其它的代碼。
與此同時,咱們知道瀏覽器是事件驅動的 (Event driven) ,瀏覽器中不少行爲是異步(Asynchronized)的,當異步事件發生時,如鼠標點擊事件,setTimeout延時觸發,Ajax返回,觸發回調等,它會建立事件並通知JavaScript引擎把事件回調函數放到執行隊列中,等待當前代碼執行完成後去執行。JavasSript的任務隊列就是由普通函數和回調函數構成的。
這就是JavaScript單線程與回調函數的執行特色,這一點也被後臺的NodeJS解釋器(原本就是瀏覽器的解釋器,因此同樣是確定的)繼承了,它們這個方面的特性是一模一樣。
到這裏,咱們再分析一下同窗們經常使用的setTimeout(func, 0)的做用,從上面的原理來看,setTimeout(func, 0)神奇之處就是告訴JavaScript引擎,在0ms之後把func放到任務隊列中,等待當前的代碼執行完畢再執行,這裏的重點是改變了代碼的執行流程,這樣就可能完成一些特殊的功能。前端
好了,分析完瀏覽器,下面開始看看這些方面對腳本影響。java
腳本組織 - 靜態結構與動態組織
1. 腳本的位置
咱們知道JavaScript代碼是放在script元素中的,能夠直接嵌套在該元素中,也能夠經過src去引用外部的js文件。 git
咱們看看script元素能夠放置的位置。
1). 放置在head區域
經過上面瀏覽器特徵的分析,這個區域不太好,由於會阻塞下面body的渲染。
2). 放置在body區域
經過上面瀏覽器特徵的分析,把腳本放到body的最後是比較好的,由於不會阻塞上面的元素的渲染。 github
2. 腳本的數量
因爲每一個<script>標籤下載時都會阻塞頁面渲染,因此減小頁面包含的<script>標籤數量有助於改善這一狀況。這不只針對外鏈腳本,內嵌腳本的數量一樣也要限制。瀏覽器在解析HTML頁面的過程當中每遇到一個<script>標籤,都會因執行腳本而致使必定的延時,所以最小化延遲時間將會明顯改善頁面的整體性能。
這個問題在處理外鏈 JavaScript 文件時略有不一樣。考慮到HTTP請求會帶來額外的性能開銷,所以下載單個100Kb的文件將比下載 5個20Kb的文件更快。也就是說,減小頁面中外鏈腳本的數量將會改善性能。
一般一個大型網站或應用須要依賴數個JavaScript文件。您能夠把多個文件合併成一個,這樣只須要引用一個<script>標籤,就能夠減小性能消耗。
可是還須要注意的是,若是這個合併的文件過大的話,可能會致使一次的下載時間太長而帶來別的問題,這時咱們就要考慮採用別的方法來提升效率了,好比動態加載。 web
3. 動態加載
動態加載,從我我的角度來講就是按需加載,頁面打開的時候先加載必須的一些腳本,而後當須要的時候,再加載後續的腳本,固然了,這些腳本也能夠以異步的方式在背後先加載好,當須要的時候直接使用便可。看一個動態加載的例子: api
(function() { var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = 'http://yourdomain.com/script.js'; var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })();
腳本的異步與延遲執行
1. 異步加載
上面同步加載腳本的過程是至關耗時的,若是咱們能同時加載多個腳本文件,並且不阻塞頁面的渲染線程,是否是能提升效率呢?這就是異步加載的思路,異步加載又叫非阻塞加載,瀏覽器在下載執行JavaScript代碼的同時,還會繼續進行後續頁面的處理。當同步嚴重阻礙產品的可用性的時候,異步是勢在必行的,這是軟件技術發展的一個基本模式。實現腳本的異步加載有不少方式,下面逐一來看一下。
1). 使用script自身特性
HTML5爲<script>標籤訂義了一個新的擴展屬性:async。它可以異步地加載和執行腳本,不由於加載腳本而阻塞頁面的加載。可是有一點須要注意,在有async的狀況下,JavaScript腳本一旦下載好了就會執行,因此頗有可能不是按照本來的順序來執行的。若是 JavaScript腳本先後有依賴性,使用async就頗有可能出現錯誤。看個例子: 瀏覽器
<script src="demo_async.js" async="async"></script>
與async用於類似功能的另外一個屬性是defer,它也是異步的加載腳本,看個例子:
<script src="file.js" defer="defer"></script>
不過只有IE和FirFox支持defer屬性,其餘的瀏覽器不支持,因此這裏就很少講了,由於咱們開發的網站可不只僅只能在IE和FireFox上能用。
2). 動態異步加載
把上一個例子中的HTML代碼換成JavaScript代碼就變成動態異步加載了,就是咱們上面的動態加載的代碼,這裏再拷貝一遍:
(function() { var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = 'http://yourdomain.com/script.js'; var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })();
可是,這種實現的加載方式在加載執行完以前會阻止onload事件的觸發,而如今不少頁面的代碼都在onload時還要執行額外的渲染工做等,因此仍是會阻塞部分頁面的初始化處理。因而把加載的時機放到onload觸發後就比較好了,大概就是這樣:
(function() { function async_load() { var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = 'http://yourdomain.com/script.js'; var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); } if (window.attachEvent) { window.attachEvent('onload', async_load); } else { window.addEventListener('load', async_load, false); } })();
這裏的關鍵就是掛接事件的那幾句代碼,這裏的實現代碼中,它不是當即開始異步加載js,而是在onload事件觸發時纔開始異步加載。這樣就解決了阻塞onload事件觸發的問題。
這裏須要理解的是onload事件是在頁面的全部資源都加載完畢(包括圖片)後觸發的,這時瀏覽器的載入進度已中止了。與這個事件相關的另一個事件是DOMContentLoaded事件,這個事件是在頁面(document)已經解析完成,頁面中的dom元素已經可用是觸發的,可是這個時候頁面中引用的圖片、subframe等可能尚未加載完。
3). 使用Ajax實現加載
提及異步加載,咱們不得不提到Ajax,使用Ajax能夠輕鬆實現異步加載,而使用JQuery的實現無疑更是簡單明瞭。看下面的例子:
// 使用Ajax方式實現異步加載 var xhr = new XMLHttpRequest(); xhr.open("get", "script1.js", true); xhr.onreadystatechange = function() { if (xhr.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); // 使用JQuery簡化加載過程 // 獲取並執行一段腳本的快捷方法:jQuery.getScript( url, [callback] ) // 方法至關於: jQuery.get(url, null, [callback], 'script') $.getScript('../scripts/Sample.js', function(data, textStatus) { alert(data); alert(textStatus); });
這個在總結Ajax與JQuery的着重說過了,再也不囉嗦。
4). 模塊化管理 - 解決順序問題
異步加載,須要將全部js內容按模塊化的方式來切分組織,其中每一個js文件就可能存在依賴關係,而異步加載不保證執行順序,這就是個問題了。解決這個問題常見的就是使用模塊化管理腳本,加上異步的特性,就是異步模塊化管理。
異步模塊化管理的概念,也就是AMD (Asynchronous Module Definition)的設計理念已經在目前較爲流行的前端框架中大行其道了,像JQuery, Dojo, MooTools, EmbedJS 這種大型的框架紛紛在其最新版本中加入了對AMD的支持。
同時,一些只注重模塊管理的精悍型類庫也不斷涌現,好比國外的RequireJS框架,它就只使用AMD的特性。使用RequireJS能夠幫助用戶異步按需的加載JavaScript代碼,並解決JavaScript模塊間的依賴關係,提高了前端代碼的總體質量和性能。
下面這幾篇文章講述了RequireJS框架的用法:
http://www.ibm.com/developerworks/cn/web/1209_shiwei_requirejs/
http://makingmobile.org/docs/tools/requirejs-api-zh/
http://requirejs.org/
而國內的同行們在這方面也在不斷努力,終於SeaJS框架橫空出世,下面幾篇教程就是講述SeaJS的用法:
http://seajs.org/
http://www.zhangxinxu.com/sp/seajs/docs/zh-cn/index.html
http://www.2cto.com/kf/201312/268256.html
2. 延遲執行
無論是同步加載,仍是異步加載,腳本加載完了就會當即執行,若是咱們想在須要的時候才執行的話,就須要採用一些特殊的手段,看下面的例子:
var se = new Image(); se.onload = registerScript(); se.src = 'http://anydomain.com/A.js';
它利用了圖片的src指向的資源是異步下載的特色實現了異步加載,同時利用onload來註冊腳本,而後在須要的時候再使用便可。
不過,咱們一般也能夠在腳本代碼中使用自執行函數把實際的功能封裝起來變成一個對象返回回來,這樣在須要的時候咱們就能夠調用這個對象去完成必定的功能,就像這樣:
var app.util = (function() { var f1 = function() {}; var f2 = function() {}; return { F1: f1, F2: f2 }; })();
3. 壓縮
這個通常指兩個方面:
第一方面:文件自身的壓縮
由於腳本等文件(Js, css, html, xml, text, inline script, 一個都不能少)是要在網上傳輸的,那麼腳本中的空格,備註,長變量其實都是對傳輸效率有害的。因此咱們須要對文件經行壓縮。這個是經過相關的工具實現的,通常只要選擇成熟的工具,細節就不用咱們管了,好比下列的一些工具:
http://webmedia.blog.163.com/blog/static/416695020123202150472/
http://yui.github.io/yuicompressor/
https://developers.google.com/speed/articles/compressing-javascript?csw=1
https://developers.google.com/closure/compiler/?csw=1
http://www.cnblogs.com/JeffreyZhao/archive/2009/12/09/ikvm-google-closure-compiler.html
第二方面:GZIP壓縮
對頁面GZIP壓縮幾乎是每篇講解高性能WEB程序的幾大作法之一,由於使用GZIP壓縮能夠下降服務器發送的字節數,能讓客戶感受到網頁的速度更快,也減小了對帶寬的使用狀況。固然,這裏也存在客戶端的瀏覽器是否支持它(大多數緣由是因爲accept-encoding head頭會丟失,因爲防火牆,安全過濾軟件等緣由)。所以,咱們一般要作的是,若是客戶端支持GZIP,咱們就發送GZIP壓縮過的內容,若是不支持,咱們直接發送靜態文件的內容。服務器端能夠配置兩套代碼,經過accept-encoding的信息來判斷。GZIP應該是在部署的時候直接部署壓縮過的文件。而不是執行壓縮代碼來壓縮文件。那樣會消耗服務器的性能。
不過目前彷佛主流的瀏覽器默認都是支持GZIP的,因此咱們一般也只須要在服務端開啓GZIP壓縮就能夠了,下面就是一篇教程:
http://www.chinaz.com/web/2012/1017/278682.shtml