面對開發(fā)者的瀏覽器的Js性能可以說,最重要的是可用性問題。這個問題是復(fù)雜的,因為Js的阻塞特性,換句話說當(dāng)Js代碼正在被執(zhí)行的時候,任何其他事情都不會發(fā)生。事實上,大多數(shù)瀏覽器使用單個進程來處理UI更新和Js執(zhí)行,所以在任何時刻只有一個能發(fā)生。Js占用約多的時間來執(zhí)行,瀏覽器能夠響應(yīng)用戶操作之前等待的時間就越長。
在基礎(chǔ)層面,意味著每一個<script>標(biāo)簽都會使頁面等待腳本被解析和執(zhí)行,不論Js代碼是內(nèi)聯(lián)的還是外聯(lián)的。頁面下載和渲染必須停止并等待腳本完成執(zhí)行,這個是頁面聲明周期中必要的部分,因為腳本執(zhí)行過程中引起頁面改變。典型的例子是在頁面中使用document.write()
,比如:
<html>
<head>
<title>Script Example</title>
</head>
<body>
<p>
<script type="text/javascript">
document.write("The date is "+ (new Date()).toDateString());
</script>
</p>
</body>
</html>
當(dāng)瀏覽器碰到<script>標(biāo)簽時,沒有辦法知道Js是否會在<p>標(biāo)簽內(nèi)插入內(nèi)容,因此瀏覽器停止處理頁面,執(zhí)行Js代碼,然后繼續(xù)解析和渲染頁面。
腳本位置
傳統(tǒng)的,<script>標(biāo)簽放在<head>標(biāo)簽內(nèi)用于加載Js文件,<link>tags加載外部的css文件。比如:
<html>
<head>
<title>Script Example</title>
<-- Example of inefficient script positioning -->
<script type="text/javascript" src="file1.js"></script>
<script type="text/javascript" src="file2.js"></script>
<script type="text/javascript" src="file3.js"></script>
</head>
<body>
<p>Hello world!</p>
</body>
</html>
雖然這段代碼看上去無害的,它實際上有一個嚴(yán)重的性能問題:有三個Js文件在<head>中被加載,因為每一個<script>標(biāo)簽阻塞頁面繼續(xù)渲染,直到腳本完全地被下載和執(zhí)行。記住,在打開<body>標(biāo)簽之前,瀏覽器不會呈現(xiàn)任何頁面內(nèi)容。把Js放在頁面頭部導(dǎo)致了顯著的延遲,通常用戶還沒有開始閱讀或者與頁面交互之前,就會出現(xiàn)空白的頁面。為了懂得如何發(fā)生了,查看下每個資源下載的瀑布流圖還是很有用的
現(xiàn)在瀏覽器都允許并行下載Js文件了,這是一個好消息,因為<script>標(biāo)簽不必阻塞其他<script>標(biāo)簽下載外部資源了。不幸的是,依然阻塞其他資源的下載,比如圖片。即使下載Js不阻塞其他資源下載,頁面還是必須等待Js代碼下載和執(zhí)行。問題還是沒有解決。
因為腳本阻塞下載頁面其他資源,所以推薦把所有的<script>標(biāo)簽盡可能放在<body>標(biāo)簽的底部,以不影響整個頁面的下載,比如:
<html>
<head>
<title>Script Example</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<p>Hello world!</p>
<-- Example of recommanded script positioning -->
<script type="text/javascript" src="file1.js"></script>
<script type="text/javascript" src="file2.js"></script>
<script type="text/javascript" src="file3.js"></script>
</body>
</html>
合并腳本
因為每一個<script>標(biāo)簽在頁面初始化下載的時候阻塞頁面渲染,限制頁面上<script>標(biāo)簽的數(shù)量是有幫助的。
每一個Http請求帶來額外的性能開銷,所以下載單個100KB的文件要比下載4個25KB的文件來得快。所以,現(xiàn)在外部Js文件的數(shù)量可以提升頁面的性能。
典型的大網(wǎng)站或web應(yīng)用程序會有多個Js文件請求。你可以通過合并這些文件為一個Js文件,然后使用一個<script>標(biāo)簽引入,來最小化性能影響。
比如,雅虎創(chuàng)建了用于分發(fā)Yahoo組合的處理程序,如何網(wǎng)站可以通過一個combo-handled URL拉取任何數(shù)量的YUI文件。比如:
<html>
<head>
<title>Script Example</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<p>Hello world!</p>
<-- Example of recommanded script positioning -->
<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.7.0/build/yahoo/yahoo-min.js&2.7.0/build/event/event-min.js"></script>
</body>
</html>
這個URL加載了2.7.0版本的yahoo-min.js和event-min.js文件。這些文件在服務(wù)器上分開存放,但是當(dāng)URL請求的時候合并在一起返回。使用單個<script>標(biāo)簽來加載,而不是通過兩個<script>標(biāo)簽。
非阻塞Script
保持Js文件小并且限制http請求數(shù)量只是對于小型網(wǎng)站的第一步。對于有這豐富功能的應(yīng)用,有更多的Js代碼請求,所以保持源代碼小不總是一個選擇。限制只下載單個大的Js文件將導(dǎo)致鎖死瀏覽器一個較長的時間,盡管它只有一個HTTP請求。為了解決這種情況,你需要使用一種非阻塞瀏覽器的方式在頁面中增量的添加更多的Js。
非阻塞腳本的秘密是在頁面加載完成后加載Js。從技術(shù)層面講,就是在window's load事件觸發(fā)后下載Js代碼,有幾個技術(shù)可以做到。
Deferred Script
<script type="text/javascript" src="file1.js" defer></script>
一個帶有defer的<script>標(biāo)簽可以放置在文檔的任何位置,Js文件會在<script>標(biāo)簽被解析后開始下載,但是不會執(zhí)行,知道DOM被完全加載(在onload事件之前執(zhí)行)。當(dāng)deferred腳本文件被下載的時候,它不阻塞瀏覽器,所以頁面上的其他資源能并行下載。
動態(tài)腳本元素
Dom允許你使用js動態(tài)創(chuàng)建HTML文檔的幾乎任何部分,當(dāng)然包括<script>標(biāo)簽。一個新的<script>標(biāo)簽可以使用標(biāo)準(zhǔn)的DOM方法來簡單的創(chuàng)建:
var script = document.createElement("script");
script.type="text/javascript";
script.src="file1.js";
document.getElementsByTagName("head")[0].appendChild(script);
這個新的<script>元素加載源文件file1.js,一旦元素被添加到頁面,文件就開始下載。關(guān)于這個技術(shù)最重要的是文件下載和執(zhí)行不會阻塞頁面其他進程,不管下載在哪兒初始化。你甚至能夠放置你的代碼在文檔<head>中,也不會影響頁面其他部分。
當(dāng)使用動態(tài)腳本節(jié)點加載的文件下載完成,這代碼就會立刻執(zhí)行,當(dāng)腳本是獨立有效的,這會工作的很好。但可能腳本的執(zhí)行依賴于頁面上其他腳本,在這種情況下,你需要跟蹤代碼被完全下載并準(zhǔn)備使用的狀態(tài),這是通過事件觸發(fā)來完成的(腳本加載完成會觸發(fā)load事件)。
var script=document.createElement("script");
script.type="text/javascript";
script.onload=function(){
alert("Script loaded!");
};
script.src="file1.js";
document.getElementsByTagName("head")[0].appendChild(script);
通常,你會考慮使用簡單的方法去動態(tài)加載Js文件,下面是一個方法封裝:
function loadScript(url,callback){
var script =document.createElement("script");
script.type="text/javascript";
if(script.readyState){
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);
}
你可能動態(tài)加載很多JS文件,但是你得考慮文件加載的順序。主流的瀏覽器為確保腳本執(zhí)行的順序會按照你指定的來,其他瀏覽器下載和執(zhí)行腳本的順序由他們被服務(wù)器返回的先后順序決定。你可以通過鏈?zhǔn)较螺d來控制順序,比如:
loadScript("file1.js",function(){
loadScript("file2.js",function(){
loadScript("file3.js",function(){
alert("All files are loaded!");
})
})
})
XMLHttpRequest 腳本注入
另一個非阻塞檢索腳本的方法是使用XMLHttpRequest對象,然后注入腳本到頁面。
var xhr = new XMLHttpRequest();
xhr.open("get","file1.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);
這個方法主要的優(yōu)勢是你可以下載腳本代碼,但不需要立刻執(zhí)行它。因為代碼不是通過<script>標(biāo)簽下載的,它不會一旦下載就立刻執(zhí)行,允許你延遲它的執(zhí)行,直到你已經(jīng)準(zhǔn)備好讓它執(zhí)行。