js在瀏覽器中性能,可以認為是開發者所面臨的最嚴重的可用性問題了,這個問題因為js的阻塞特性變得很復雜,也就是說瀏覽器在執行js代碼時,不能同時做其他任何事情。事實上,多數瀏覽器使用單一進程來處理用戶界面刷新和js腳本的執行,所以只能同一時刻做一件事,js的執行過程耗時越久,瀏覽器等待響應的時間就越長。
簡單的說,這意味着<script>標簽每次出現都霸道地讓頁面等待腳本的解析和執行。無論當前的js代碼時內嵌還是外鏈接,頁面的下載和渲染都必須停下來等腳本的執行完成。這是頁面生存周期中的必要環節,因為腳本執行過程中可能會修改頁面內容。一個典型例子就死document.write().我們看到的廣告就是這么搞的。
腳本的位置
html4規范指出<script>標簽可以放在html文檔的<head>或<body>中,並允許出現多次。按照慣例,<script>標簽用來加載出現在css加載的<link>標簽后。理論上來說,把樣式和行為有關的腳本放在一起,並先加載它們,這樣做有助於頁面的渲染和交互的正確性。
但是,這樣存在十分嚴重的性能問題,在<head>標簽中加載js文件,由於腳本會阻塞頁面的渲染,直到它們全部下載並執行完成后,頁面的渲染的才會執行。要知道,瀏覽器在解析到<body>標簽之前,不會渲染頁面的任何內容,把腳本放在頁面頂部會導致明顯的延遲,會有明顯的白屏時間,用戶無法瀏覽內容,也無法與頁面進行交互。瀑布圖可以幫我們更清楚地理解性能發生的原因。因此js要放在<body>標簽的底部。
組織腳本
每一個<script>標簽初始下載時都會阻塞頁面渲染,所以減少頁面包含的<script>標簽數量有助於改善這一情況,這不僅僅是針對外鏈腳本,內鏈腳本的數量也要限制,這個問題在處理外鏈腳本文件時略有不同,因為http請求還會帶來額外的性能開銷,因此下載單個100kb的文件將比下載四個25kb的文件更快,也就是說,減少頁面中腳本文件數量將會改善性能。
通常一個大型網站或網絡應用需要依賴數個js文件,我們可以把多個文件合並成一個,這樣就只需引用一個<script>標簽了。文件合並可以利用現在的很多構建工具,grunt,gulp等,都很方便。
無阻塞的腳本
js傾向於阻止瀏覽器的某些處理過程,如http請求和用戶界面更新,這是開發者所面臨的最顯著的性能問題。減少js文件大小並限制http請求僅僅是創建響應迅速的Web應用的第一步,web應用的功能越來越強大豐富,所需要的腳本代碼也就越多,所以精簡代碼並不總是可行,盡管下載單個較大的js文件只產生一次http請求,卻會鎖死瀏覽器一大段時間,這樣顯然不是良好的用戶體驗,為避免這種情況,我們需要的是向頁面中逐步加載js文件,這樣做從某種程度上不會阻塞瀏覽器。
無阻塞腳本的秘訣在於,在頁面加載完后才加載js代碼,用專業術語說,這意味着window對象的load事件觸發后再下載腳本,有很多方式可以實現這一效果。
《1》延遲的腳本
html4為<script>標簽定義了一個擴展屬性,defer。defer屬性指明本元素所含的腳本不會修改dom,因此代碼可以安全的延遲執行。這個屬性目前已經被所有的主流瀏覽器支持了。另外說說HTML5 中引入的async屬性,用於異步加載腳本。async和defer的相同點是采用並行下載,在下載的過程不會產生阻塞,區別在於執行的時機,async是加載完成后自動執行,而defer需要等待頁面完成后才執行。
帶有defer屬性的<script>標簽可以放置在文檔的任何位置,對應的js文件將在解析到<script>標簽時開始下載,但不會執行,直到dom加載完成后(onload事件被觸發前)因此這類文件可以與頁面中的其他資源並行下載。
<script type='type/javascript ' src="xiaoai.js" defer></script>
示例:
<html>
<head>
<title> script defer</title>
</head>
<body>
<script defer>
alert(1);
</script>
<script>
alert(2);
</script>
<script>
window.onload=function(){
alert(3);
}
</script>
</body>
</html>
這段代碼彈出三次提示框,若你的瀏覽器支持defer,彈出的順序為2,1,3;而不支持defer的的瀏覽器則是1,2,3。請注意,帶有defer屬性的瀏覽器不是跟在第二個執行,而是在onload事件之前執行。
《2》動態腳本
由於DOM的存在,你可以用js創建HTML中幾乎所有內容。其原因在於,<script>元素與頁面其他元素並無差異:都能通過DOM進行引用,都能在文檔中移動,刪除或是被創建。用標准的DOM方法可以很容易的創建一個新的<script>元素:
var script=document.createElement(‘script’);
script.type="text/javascript";
script.src="file1.js";
document.getElementsByTagName('head')[0].appendChild(script);
這個新創建的<script>元素加載了file1.js文件。文件在該元素被添加到頁面時開始下載。這種技術的重點在於:無論在何時啟動下載,文件的下載和執行過程不會阻塞頁面的其他進程。你甚至可以將代碼放到頁面<head>區域而不會影響頁面其他部分(用於下載文件的http鏈接本身的影響除外)。
另外,要注意,把新創建的<script>標簽添加到<head>標簽里比添加到<body>里更保險,尤其是在頁面加載過程中執行代碼時更是如此。當<body>中的內容沒有加載完成時,IE會拋出“操作已終止”的錯誤信息。
使用動態腳本節點下載文件時,返回的代碼通常會立即執行(除了Firefox和opera,它們會等待此前所有動態腳本節點執行完畢)。當腳本‘自執行’時,這種機制運行正常。但是當代碼只包含供頁面其他腳本調用的接口時,就會有問題。在這種情況下,你必須跟蹤並確保腳本下載完成且准備就緒。這可以用動態<script>節點觸發的事件來實現。
Firefox,opera,Chrome和Safari以上的版本會在<script>元素接收完成時觸發一個load事件。因此可以通過偵聽此事件來獲得腳本加載完成時的狀態;
var script=document.createElement('script')
script.type='text/javascript';
script.onload=function(){
alert("script loaded");
};
script.src='file2.js';
document.getElementByTagName('head')[0].appendChild(script);
IE支持另一種實現方式,它會觸發一個readyStatechange事件。<script>元素提供一個readyState屬性,它的值在外鏈文件的下載過程的不同階段會發生變化,該屬性有五種取值:
“uninitialized” 初始狀態
“loading” 開始下載
“loaded” 下載完成
“interactive” 數據完成下載但尚不可用
“complete” 所有數據已准備就緒
微軟的相關文檔表明,<script>元素生命周期中,並非readyState的每個取值都會被用到,實際應用中,最有用的兩個狀態就是“loaded”和“complete”。Ie在標識最終狀態時的值並不一致,有時<script>元素達到“loaded”狀態而從不會到達“complete”,有時候直接跳到“complete”而不經過“loaded”,使用這個屬性時最靠譜的方式是同時檢查這兩個狀態,只要其中任何一個觸發,就刪除事件處理器(以確保不會處理兩次)。
var script=document.createElement('script')
script.type="text/javascript";
script.onreadystatechange=function(){
if(script.readyState=="loaded"||script.readyState=="complete"){
script.onreadystatechange=null;
alert('script loaded');
};
script.src='file3.js';
document.getElementsByTagName('head')[0].appendChild(script);
}
以上是針對IE的動態加載js文件方法。
我們需要一個兼容各瀏覽器的動態加載js文件的方法,下面是一個函數封裝了標准和IE特有的實現方法
function loadscript(url,callback)
{
var script=document.createElement('script')
script.type='text/javascript';
if(script.readyState){//IE
script.onreadystatechange=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);
}
這個函數接收兩個參數:JavaScript文件的URL和完成加載后的回調函數。函數中使用了特征檢測來決定腳本處理過程中監聽哪個事件。最后一步是給src屬性賦值,然后將<script>元素添加到頁面。loadscript()函數用法如下
loadscript("file1.js",function(){
alert(‘file is loaded’);
});
如果需要的話,你可以動態加載盡肯能多的jswenjian 到頁面上,但一定要考慮清楚文件的加載順序。在所有的主流瀏覽器中,只有Firefox和opera能保證腳本會按照你指定的順序執行,其他瀏覽器會按照從服務端返回的順序下載和執行代碼。因此可以通過下面的串聯方式以確保下載順序。
loadscript(‘file1.js’,function(){
loadscript('file2.js',function(){
loadscript("file3.js",function(){
alert('all file is loaded');
});
});
});
下載順序為 file1,file2,file3。
如果多個文件的下載順序很重要,更好的做法是把他們按正確的順序合並成一個文件。下載這個文件就會獲得所有的代碼(由於這個過程是異步的,因此文件大點沒關系)
總而言之,動態腳本加載憑借着它在跨瀏覽器兼容性和易用的優勢,成為最通用的無阻塞加載js的解決方案。
《3》XMLhttpRequest 腳本注入
另一種無阻塞加載腳本的方法是使用XMLHttpRequest(XHR)對象獲取腳本並注入頁面中。
此技術胡創建一個XHR對象,然后用它下載JavaScript文件,最后通過創建動態<script>元素將代碼注入到頁面中。
var xhr =new XMLHttpRequest();
xhr.open('get','file1.js',true);
xhr.onreadystatechange=funcition(){
if(xhr.readyState==4){
if(xhr.status>=200&&xhr.status<300||xhr.status==304){
var script=document.creat.createElement('script');
script.type="text/javascript";
script.text=xhr.responseText;
document.body.appendChild(script);
}
}
};
chr.send(null);
這段代碼發送一個GET請求獲取file.js文件。事件處理函數onreadychange檢查readyState是否為4,同時檢驗http狀態碼是否有效(2xx代表有效響應,304代表從緩存中讀取)。如果收到了有效響應,就會創建一個<script>元素,設置該元素的text屬性為從服務器接收到的resposeText。這樣實際上是創建一個帶有內聯腳本的<script>標簽。一旦新創建的<script>元素被添加到頁面,代碼就會立刻執行然后准備就緒。
這種方法的優點是:你可以下載JavaScript代碼但不立即執行。由於代碼是在<script>標簽之外返回的,因此它下載后不會自動執行,這使得你可以把腳本執行推遲到你准備好的時候。另一個優點是,同樣的代碼在所有瀏覽器都能正常工作。
這種方法的局限性是:js文件必須與所請求的頁面處於相同的域,這意味着js文件不能從cdn下載。因此,大型的web應用通常不會采用XHR腳本注入技術。
小結:
管理瀏覽器中js代碼是個棘手的問題,因為代碼執行過程會阻塞瀏覽器的其他進程,比如用戶界面繪制。每次遇到<script>標簽,頁面都必須停下來等待所有的js代碼下載並執行,然后恢復處理。盡管如此,還是有幾種方法能減少js對性能的影響:
1.<body>閉合標簽之前,將所有<script>標簽放在頁面底部。這能確保腳本執行前頁面已經完成渲染。
2.合並腳本。頁面中<script>標簽越少,加載也就越快,響應也更迅速。無論外鏈文件還是內嵌的腳本都是如此。
3.有多種無阻塞下載js的方法:
---<script defer>
---使用動態創建的<script>元素來下載並執行代碼
---使用XHR對象下載js代碼並注入頁面中。