標籤:廣告 時間 動態建立 非同步 多次 文檔 開始 bsp css
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代碼並注入頁面中。
關於js的執行與載入