前言
為什么要采用js來create一個script標簽,設置src然后append到head中,而不是直接使用script標簽,這樣不是更簡單點嗎?
javascript的裝載和執行
首先,我想說一下Javascript的裝載和執行。通常來說,瀏覽器對於Javascript的運行有兩大特性:
1)載入后馬上執行
2)執行時會阻塞頁面后續的內容(包括頁面的渲染、其它資源的下載)
於是,如果有多個js文件被引入,那么對於瀏覽器來說,這些js文件被被串行地載入,並依次執行。因為javascript可能會來操作HTML文檔的DOM樹,所以,瀏覽器一般都不會像並行下載css文件並行下載js文件,因為這是js文件的特殊性造成的。所以,如果你的javascript想操作后面的DOM元素,基本上來說,瀏覽器都會報錯說對象找不到。因為Javascript執行時,后面的HTML被阻塞住了,DOM樹時還沒有后面的DOM結點。所以程序也就報錯了。
【小結】:默認情況下,瀏覽器對js串行加載、css並行加載
Script 的堵塞(block)特性
Scripts without async or defer attributes, as well as inline scripts, are fetched and executed immediately, before the browser continues to parse the page. - MDN
the blocking nature of JavaScript, which is to say that nothing else can happen while JavaScript code is being executed. In fact, most browsers use a single process for both user interface (UI) updates and JavaScript execution, so only one can happen at any given moment in time. The longer JavaScript takes to execute, the longer it takes before the browser is free to respond to user input. - Nicholas C. Zakas「High Performance JavaScript 」
上面引用兩段話的意思大致是,當瀏覽器解析DOM文檔時,一旦遇到 script 標簽(沒有defer 和 async 屬性)就會立即下載(如果是外部文件)並執行,與此同時瀏覽器對文檔的解析將會停止,直到 script 代碼執行完成。出現這種堵塞行為一方面是因為瀏覽器的UI渲染,交互行為等都是單線程操作,另一方是因為 script 里面的代碼可能會影響到后面文檔的解析,例如清單 1
清單 1 JavaScript 代碼內嵌示例
<script type="text/javascript"> document.write("The date is " + (new Date()).toDateString()); </script>
|
無論當前 JavaScript 代碼是內嵌還是在外鏈文件中,頁面的下載和渲染都必須停下來等待腳本執行完成。JavaScript 執行過程耗時越久,瀏覽器等待響應用戶輸入的時間就越長。瀏覽器在下載和執行腳本時出現阻塞的原因在於,腳本可能會改變頁面或 JavaScript 的命名空間,它們對后面頁面內容造成影響。一個典型的例子就是在頁面中使用document.write()
1
2
3
4
5
6
7
8
9
10
11
12
|
< html >
< head >
< title >Source Example</ title >
</ head >
< body >
< p >
<script type="text/javascript">
document.write( "Today is " + ( new Date()).toDateString());
</script>
</ p >
</ body >
</ html >
|
當瀏覽器遇到<script>
標簽時,當前
HTML 頁面無從獲知
JavaScript 是否會向
<p>
標簽添加內容,或引入其他元素,或甚至移除該標簽。因此,這時瀏覽器會停止處理頁面,先執行
JavaScript代碼,然后再繼續解析和渲染頁面。同樣的情況也發生在使用
src
屬性加載
JavaScript的過程中,瀏覽器必須先花時間下載外鏈文件中的代碼,然后解析並執行它。在這個過程中,頁面渲染和用戶交互完全被阻塞了。
腳本位置
HTML 4 規范指出 <script>
標簽可以放在 HTML 文檔的<head>
或<body>
中,並允許出現多次。Web 開發人員一般習慣在 <head>
中加載外鏈的 JavaScript,接着用 <link>
標簽用來加載外鏈的 CSS 文件或者其他頁面信息。例如清單 2
清單 2 低效率腳本位置示例
1
2
3
4
5
6
7
8
9
10
11
12
|
< html >
< head >
< title >Source Example</ title >
<script type="text/javascript" src="script1.js"> </script>
<script type="text/javascript" src="script2.js"> </script>
<script type="text/javascript" src="script3.js"> </script>
< link rel = "stylesheet" type = "text/css" href = "styles.css" >
</ head >
< body >
< p >Hello world!</ p >
</ body >
</ html >
|
然而這種常規的做法卻隱藏着嚴重的性能問題。在清單 2 的示例中,當瀏覽器解析到 <script>
標簽(第 4 行)時,瀏覽器會停止解析其后的內容,而優先下載腳本文件,並執行其中的代碼,這意味着,其后的 styles.css 樣式文件和<body>
標簽都無法被加載,由於<body>
標簽無法被加載,那么頁面自然就無法渲染了。因此在該 JavaScript 代碼完全執行完之前,頁面都是一片空白。圖 1 描述了頁面加載過程中腳本和樣式文件的下載過程。
圖 1 JavaScript 文件的加載和執行阻塞其他文件的下載
我們可以發現一個有趣的現象:第一個 JavaScript 文件開始下載,與此同時阻塞了頁面其他文件的下載。此外,從 script1.js 下載完成到 script2.js 開始下載前存在一個延時,這段時間正好是 script1.js 文件的執行過程。每個文件必須等到前一個文件下載並執行完成才會開始下載。在這些文件逐個下載過程中,用戶看到的是一片空白的頁面。
從 IE 8、Firefox 3.5、Safari 4 和 Chrome 2 開始都允許並行下載 JavaScript 文件。這是個好消息,因為<script>
標簽在下載外部資源時不會阻塞其他<script>
標簽。遺憾的是,JavaScript 下載過程仍然會阻塞其他資源的下載,比如樣式文件和圖片。盡管腳本的下載過程不會互相影響,但頁面仍然必須等待所有 JavaScript 代碼下載並執行完成才能繼續。因此,盡管最新的瀏覽器通過允許並行下載提高了性能,但問題尚未完全解決,腳本阻塞仍然是一個問題。
由於腳本會阻塞頁面其他資源的下載,因此推薦將所有<script>
標簽盡可能放到<body>
標簽的底部,以盡量減少對整個頁面下載的影響。例如清單 3
清單 3 推薦的代碼放置位置示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
< html >
< head >
< title >Source Example</ title >
< link rel = "stylesheet" type = "text/css" href = "styles.css" >
</ head >
< body >
< p >Hello world!</ p >
<!-- Example of efficient script positioning -->
<script type="text/javascript" src="script1.js"> </script>
<script type="text/javascript" src="script2.js"> </script>
<script type="text/javascript" src="script3.js"> </script>
</ body >
</ html >
|
這段代碼展示了在 HTML 文檔中放置<script>
標簽的推薦位置。盡管腳本下載會阻塞另一個腳本,但是頁面的大部分內容都已經下載完成並顯示給了用戶,因此頁面下載不會顯得太慢。這是優化 JavaScript 的首要規則:將腳本放在底部。
組織腳本
由於每個<script>
標簽初始下載時都會阻塞頁面渲染,所以減少頁面包含的<script>
標簽數量有助於改善這一情況。這不僅針對外鏈腳本,內嵌腳本的數量同樣也要限制。瀏覽器在解析 HTML 頁面的過程中每遇到一個<script>
標簽,都會因執行腳本而導致一定的延時,因此最小化延遲時間將會明顯改善頁面的總體性能。
這個問題在處理外鏈 JavaScript 文件時略有不同。考慮到 HTTP 請求會帶來額外的性能開銷,因此下載單個 100Kb 的文件將比下載 5 個 20Kb 的文件更快。也就是說,減少頁面中外鏈腳本的數量將會改善性能。
通常一個大型網站或應用需要依賴數個 JavaScript 文件。您可以把多個文件合並成一個,這樣只需要引用一個<script>
標簽,就可以減少性能消耗。文件合並的工作可通過離線的打包工具或者一些實時的在線服務來實現。
需要特別提醒的是,把一段內嵌腳本放在引用外鏈樣式表的<link>
之后會導致頁面阻塞去等待樣式表的下載。這樣做是為了確保內嵌腳本在執行時能獲得最精確的樣式信息。因此,建議不要把內嵌腳本緊跟在<link>
標簽后面。
無阻塞的腳本
減少 JavaScript 文件大小並限制 HTTP 請求數在功能豐富的 Web 應用或大型網站上並不總是可行。Web 應用的功能越豐富,所需要的 JavaScript 代碼就越多,盡管下載單個較大的 JavaScript 文件只產生一次 HTTP 請求,卻會鎖死瀏覽器的一大段時間。為避免這種情況,需要通過一些特定的技術向頁面中逐步加載 JavaScript 文件,這樣做在某種程度上來說不會阻塞瀏覽器。
無阻塞腳本的秘訣在於,在頁面加載完成后才加載 JavaScript 代碼。這就意味着在 window
對象的 onload
事件觸發后再下載腳本。有多種方式可以實現這一效果。
另外,因為絕大多數的Javascript代碼並不需要等頁面,所以,我們可以異步載入,那么我們怎么異步載入呢?不過更好的方法是下面的非堵塞加載腳本(Nonblocking Scripts)的方法,下面的方法其實都是異步加載:
1. script的defer和async屬性(延遲腳本/異步腳本)
一、延遲腳本
HTML 4 為<script>
標簽定義了一個擴展屬性:defer
< script defer type = "text/javascript" src = "./alert.js" > </ script >
|
defer
屬性已經被所有主流瀏覽器所支持,僅有外鏈js支持該屬性。
帶有 defer
屬性的<script>
標簽可以放置在文檔的任何位置。對應的 JavaScript 文件將在頁面解析到<script>
標簽時開始下載,但不會執行,直到 DOM 加載完成,即onload
事件觸發前才會被執行。當一個帶有 defer
屬性的 JavaScript 文件下載時,它不會阻塞瀏覽器的其他進程,因此這類文件可以與其他資源文件一起並行下載。
任何帶有 defer
屬性的<script>
元素在 DOM 完成加載之前都不會被執行,無論內嵌或者是外鏈腳本都是如此
< html >
< head >
< title >Script Defer Example</ title >
</ head >
<script type="text/javascript" src='a.js' defer> </script>
< body >
<script type="text/javascript">
alert( "script" );
</script>
<script type="text/javascript">
window.onload = function (){
alert( "load" );
};
</script>
</ body >
</ html >
|
這段代碼在頁面處理過程中彈出三次對話框。彈出的順序則是:“script”、“defer”、“load”。請注意,帶有 defer
屬性的<script>
元素不是跟在第二個后面執行,而是在 onload
事件被觸發前被調用。
總有說多個defer的<script>在執行時也會按照其出現的順序來運行,但實際上【 延腳本並不一定會按照順序執行, 也不一定會在 DOMContentLoaded 事件觸發前行, 因此最好只包含一個延遲腳本】
二、異步腳本(實際並沒有達到無阻塞)
而我們標准的的HTML5也加入了一個異步載入javascript的屬性:async,無論你對它賦什么樣的值,只要它出現,它就開始異步加載js文件。但是, async的異步加載會有一個比較嚴重的問題,那就是它忠實地踐行着“載入后馬上執行”這條軍規,所以,雖然它並不阻塞頁面的渲染,但是你也無法控制他執行的次序和時機。你可以看看這個示例去感受一下。【有一點需要注意,在有 async
的情況下,JavaScript 腳本一旦下載好了就會執行,所以很有可能不是按照原本的順序來執行的。如果 JavaScript 腳本前后有依賴性,使用 async
就很有可能出現錯誤。】
支持 async標簽的瀏覽器是:Firefox3.6+,Chrome 8.0+,Safari 5.0+,IE 10+,Opera還不支持(來自這里)所以這個方法也不是太好。因為並不是所有的瀏覽器你都能行。
【小結】:defer和async的相同點是采用並行下載,在下載的過程中不會產生阻塞,區別在於執行時機,async是加載完成后自動執行,而defer需要等待頁面完成后(DOMContentLoaded)執行。
2. Dynamic Script Elements (動態腳本)
方法一:動態創建DOM方式
這種方式可能是用得最多的了,原理就是使用腳本創建 script 元素,設置 src 需為要動態添加腳本的 URL,再把這個 script 添加到DOM中。有時我們需要動態腳本加載完成后再執行某些操作,這就需要我們在腳本加載完成后添加一個回調函數,這個可以通過 script 的 onload 事件實現。下面的實現代碼:
文檔對象模型(DOM)允許您使用 JavaScript 動態創建 HTML 的幾乎全部文檔內容。<script>
元素與頁面其他元素一樣,可以非常容易地通過標准 DOM 函數創建:
清單 6 通過標准 DOM 函數創建<script>元素
1
2
3
4
|
var script = document.createElement ("script");
script.type = "text/javascript";
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);
|
新的<script>
元素加載 script1.js 源文件。此文件當元素添加到頁面之后立刻開始下載。此技術的重點在於:無論在何處啟動下載,文件的下載和運行都不會阻塞其他頁面處理過程。您甚至可以將這些代碼放在<head>
部分而不會對其余部分的頁面代碼造成影響(除了用於下載文件的 HTTP 連接)。
當文件使用動態腳本節點下載時,返回的代碼通常立即執行(除了 Firefox 和 Opera,他們將等待此前的所有動態腳本節點執行完畢)。當腳本是“自運行”類型時,這一機制運行正常,但是如果腳本只包含供頁面其他腳本調用調用的接口,則會帶來問題。這種情況下,您需要跟蹤腳本下載完成並是否准備妥善。可以使用動態 <script>
節點發出事件得到相關信息。
Firefox、Opera, Chorme 和 Safari 3+會在<script>
節點接收完成之后發出一個 onload
事件。您可以監聽這一事件,以得到腳本准備好的通知:
清單 7 通過監聽 onload 事件加載 JavaScript 腳本
1
2
3
4
5
6
7
8
9
10
|
var script = document.createElement ("script")
script.type = "text/javascript";
//Firefox, Opera, Chrome, Safari 3+
script.onload = function(){
alert("Script loaded!");
};
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);
|
Internet Explorer 支持另一種實現方式,它發出一個 readystatechange
事件。<script>
元素有一個 readyState
屬性,它的值隨着下載外部文件的過程而改變。readyState
有五種取值:
- “uninitialized”:默認狀態
- “loading”:下載開始
- “loaded”:下載完成
- “interactive”:下載完成但尚不可用
- “complete”:所有數據已經准備好
微軟文檔上說,在<script>
元素的生命周期中,readyState
的這些取值不一定全部出現,但並沒有指出哪些取值總會被用到。實踐中,我們最感興趣的是“loaded”和“complete”狀態。Internet Explorer 對這兩個 readyState
值所表示的最終狀態並不一致,有時<script>
元素會得到“loader”卻從不出現“complete”,但另外一些情況下出現“complete”而用不到“loaded”。最安全的辦法就是在 readystatechange
事件中檢查這兩種狀態,並且當其中一種狀態出現時,刪除 readystatechange
事件句柄(保證事件不會被處理兩次):
清單 8 通過檢查 readyState 狀態加載 JavaScript 腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var script = document.createElement("script")
script.type = "text/javascript";
//Internet Explorer
script.onreadystatechange = function(){
if (script.readyState == "loaded" || script.readyState == "complete"){
script.onreadystatechange = null;
alert("Script loaded.");
}
};
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);
|
大多數情況下,您希望調用一個函數就可以實現 JavaScript 文件的動態加載。下面的函數封裝了標准實現和 IE 實現所需的功能:
清單 9 通過函數進行封裝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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 { //Others
script.onload = function(){
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
|
此函數接收兩個參數:JavaScript 文件的 URL,和一個當 JavaScript 接收完成時觸發的回調函數。屬性檢查用於決定監視哪種事件。最后一步,設置 src
屬性,並將<script>
元素添加至頁面。此 loadScript()
函數使用方法如下:
清單 10 loadScript()函數使用方法
1
2
3
|
loadScript("script1.js", function(){
alert("File is loaded!");
});
|
您可以在頁面中動態加載很多 JavaScript 文件,但要注意,瀏覽器不保證文件加載的順序。所有主流瀏覽器之中,只有 Firefox 和 Opera 保證腳本按照您指定的順序執行。其他瀏覽器將按照服務器返回它們的次序下載並運行不同的代碼文件。您可以將下載操作串聯在一起以保證他們的次序,如下:
清單 11 通過 loadScript()函數加載多個 JavaScript 腳本
1
2
3
4
5
6
7
|
loadScript("script1.js", function(){
loadScript("script2.js", function(){
loadScript("script3.js", function(){
alert("All files are loaded!");
});
});
});
|
此代碼等待 script1.js 可用之后才開始加載 script2.js,等 script2.js 可用之后才開始加載 script3.js。雖然此方法可行,但如果要下載和執行的文件很多,還是有些麻煩。
如果多個文件的次序十分重要,更好的辦法是將這些文件按照正確的次序連接成一個文件。獨立文件可以一次性下載所有代碼(由於這是異步進行的,使用一個大文件並沒有什么損失)。
動態腳本加載是非阻塞 JavaScript 下載中最常用的模式,因為它可以跨瀏覽器,而且簡單易用
這方式還被玩出了JSONP的東東,也就是我可以為script的src指定某個后台的腳本(如PHP),而這個PHP返回一個javascript函數,其參數是一個json的字符串,返回來調用我們的預先定義好的javascript的函數
方法二:按需異步載入js
上面那個DOM方式的例子解決了異步載入Javascript的問題,但是沒有解決我們想讓他按我們指定的時機運行的問題。所以,我們只需要把上面那個DOM方式綁到某個事件上來就可以了。
實現一:
綁在window.load事件上——示例四
你一定要比較一下示例四和示例三在執行上有什么不同,我在這兩個示例中都專門用了個代碼高亮的javascript,看看那個代碼高亮的的腳本的執行和我的alert.js的執行的情況,你就知道不同了)
window.load = loadjs( "https://coolshell.cn/asyncjs/alert.js" ) |
實現二:
綁在特定的事件上——示例五
< p style = "cursor: pointer" onclick = "LoadJS()" >Click to load alert.js </ p > |
這個示例很簡單了。當你點擊某個DOM元素,才會真正載入我們的alert.js。
3. XMLHttpRequest Script Injection (XHR動態插入)
原理是利用XMLHttpReques(XHR)對象,動態獲取一段JS代碼,然后插入文檔。
相對其他方法來說的一個優點是可以“懶執行”,也就是JS代碼已經先下載好了並沒有執行,可以在需要的來執行(?)(之前的動態腳本在下載后會立即執行)。實現代碼:
此技術首先創建一個 XHR 對象,然后下載 JavaScript 文件,最后通過動態創建script元素將代碼注入頁面 JavaScript 代碼注入頁面。清單 12 是一個簡單的例子:
清單 12 通過 XHR 對象加載 JavaScript 腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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);
|
此代碼向服務器發送一個獲取 script1.js 文件的 GET 請求。onreadystatechange
事件處理函數檢查 readyState
是不是 4,然后檢查 HTTP 狀態碼是不是有效(2XX 表示有效的回應,304 表示一個緩存響應)。如果收到了一個有效的響應,那么就創建一個新的<script>
元素,將它的文本屬性設置為從服務器接收到的 responseText
字符串。這樣做實際上會創建一個帶有內聯代碼的<script>
元素。一旦新<script>
元素被添加到文檔,代碼將被執行,並准備使用。
這種方法的主要優點是,您可以下載不立即執行的 JavaScript 代碼。由於代碼返回在<script>
標簽之外(換句話說不受<script>
標簽約束),它下載后不會自動執行(因為是內聯script,准備執行的js代碼不是在src中引入的,所以說代碼返回在script標簽之外),這使得您可以推遲執行,直到一切都准備好了。另一個優點是,同樣的代碼在所有現代瀏覽器中都不會引發異常。
此方法最主要的限制是:JavaScript 文件必須與頁面放置在同一個域內,(不能跨域請求)不能從 CDN 下載(CDN 指"內容投遞網絡(Content Delivery Network)",所以大型網頁通常不采用 XHR 腳本注入技術。
4. lazyLoad
詳情見:延遲加載工具:LazyLoad
5.LABjs
詳情見:無阻塞腳本加載工具:LABjs
總結
減少 JavaScript 對性能的影響有以下幾種方法:
-
將所有的
<script>
標簽放到頁面底部,也就是</body>
閉合標簽之前,這能確保在腳本執行前頁面已經完成了渲染。 -
盡可能地合並腳本。頁面中的
<script>
標簽越少,加載也就越快,響應也越迅速。無論是外鏈腳本還是內嵌腳本都是如此。 -
采用無阻塞下載 JavaScript 腳本的方法:
-
使用
<script>
標簽的 defer 屬性; -
使用動態創建的
<script>
元素來下載並執行代碼; -
使用 XHR 對象下載 JavaScript 代碼並注入頁面中。
-
通過以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 網站和應用的實際性能。
參考