JavaScript在瀏覽器中的性能,可認為是開發者所要面對的最重要的可用性的問題,此問題因JavaScript的阻塞特征而復雜,也就是說JavaScript運行時其他的事情不能被瀏覽器處理,事實上,大多數瀏覽器使用單進程處理UI更新和JavaScript運行等多個任務,而同一時間只能有一個任務被執行。JavaScript運行了多長時間,那么瀏覽器空閑下來響應用戶輸入之前的等待時間就有多長。
從基本層面說,這就意味着<script>標簽的出現使整個頁面因腳本解析、運行出現等待。不論實際的JavaScript代碼是內聯的還是包含在一個不相干的外部文件中頁面下載和解析過程必須停下,等待腳本完成這些處理,然后才能繼續,也是頁面生命周期必不可少的部分,因為腳本可能在運行過程中修改頁面內容。
在加載JavaScript過程中,頁面解析和用戶交互是被完全阻塞的。
HTML 4 文檔指出,一個<script>標簽可以放在 HTML文檔的 <head> 或者<body>標簽中,可以在其中多次出現。傳統上,<script> 標簽用於加載外部JavaScript 文件。<head>部分除此類代碼外,還包含 <link>標簽用於加載外部css文件和其他頁面中間件。也就是說,最好把風格和行為所依賴的部分放在一起,首先加載他們,使他們可以得到正確的外觀和行為。
例如:
1 <html> 2 <head> 3 <title>Script Example</title> 4 <-- Example of ineffi cient script positioning --> 5 <script type="text/javascript" src="file1.js"></script> 6 <script type="text/javascript" src="file2.js"></script> 7 <script type="text/javascript" src="file3.js"></script> 8 <link rel="stylesheet" type="text/css" href="styles.css"> 9 </head> 10 <body> 11 <p>Hello world!</p> 12 </body> 13 </html>
雖然這些代碼看起來沒什么問題,但是在〈head〉部分加載了三個JavaScript文件。每個〈script〉標簽阻塞了頁面解析過程,直到完整的下載並運行了外部JavaScript代碼之后,頁面才能繼續進行。在瀏覽器沒有遇到〈body〉標簽之前,不會渲染頁面的任何部分。
把腳本放在頁面的頂端,將會導致一個可以察覺的延遲,通常表現為:頁面打開一片白,用戶不能閱讀和操作。
如圖,當第一javas文件開始下載時,阻塞了其他文件下載。進一步當第一個文件下載完成之后和第二個文件下載之前有一個延時,是第一個文件完全運行所需要的時間。
解決這個問題推薦的辦法是:將所有<script> 標簽放在盡可能接近<body> 標簽的底部位置,盡量減少對整個頁面下載的影響。
如:
1 <html> 2 <head> 3 <title>Script Example</title> 4 <link rel="stylesheet" type="text/css" href="styles.css"> 5 </head> 6 <body> 7 <p>Hello world!</p> 8 <-- Example of recommended script positioning --> 9 <script type="text/javascript" src="file1.js"></script> 10 <script type="text/javascript" src="file2.js"></script> 11 <script type="text/javascript" src="file3.js"></script> 12 </body> 13 </html>
由於每個<script>標簽下載時阻塞頁面解析過程,所以限制頁面的<script>總數也是可以改善性能。這個規則對內聯腳本和外部腳本同樣適用。每當頁面解析碰到一個<script>標簽時,緊接着有一段時間用於代碼執行。最小化這些延遲時間可以改善頁面的整體性能。
每個HTTP請求都會產生額外的性能負擔,下載一個100KB的文件比下載4個25KB的文件要快。總之,減少引用外部文件的數量。典型的,一個大型網站或者網頁應用需要多次請求JavaScript文件。你可以將這些文件整合成一個文件,只需要一個<script>標簽引用,就可以減少性能損失。
JavaScript傾向於阻塞瀏覽器某些處理過程,如HTTP請求和界面刷新,這是開發者面臨的最顯著的性能問題。保持JavaScript文件短小,並限制HTTP請求的數量,只是創建反應迅速的網頁應用的第一步。一個應用程序所包含的功能越多,所需要的JavaScript代碼就越大,保持源碼短小並不總是一種選擇。盡可能下載一個大JavaScript文件只產生一次HTTP請求。卻會鎖住瀏覽器一大段時間。為避開這種情況,你需要向頁面中逐步添加JavaScript,某種程度上說不會阻塞瀏覽器。
非阻塞腳本的秘密在於,等頁面加載之后,再加載JavaScript源碼。從技術角度上講,這意味着在window的load事件發出之后下載代碼。有幾種方法可以實現這種效果。
1.延期腳本
HTML4為<script>標簽定義了一個擴展屬性:defer。這個defer屬性指明元素中所包含的腳本不打算修改DOM,因此代碼可以稍后執行(適用於IE4以上瀏覽器)
<script type="text/javascript" src="file1.js" defer></script>
帶有該屬性的JavaScript文件在<script>被解析時啟動下載,但代碼不會被執行,直到DOM加載完成,它不會阻塞瀏覽器的其他處理過程,所以這些文件可以與頁面的其他資源一起並行下載。
2.動態腳本元素
文檔對象模型dom允許使用JavaScript動態創建HTML的幾乎全部文檔內容。其根本在於<script>元素與頁面其他元素沒有什么不同。
當文件使用動態腳本節點下載時,返回的代碼通常立即執行。當腳本“自運行”類型時這一機制運行正常,但是如果腳本只包含頁面其他腳本調用的的接口,則會帶來問題。這種情況下,你需要跟蹤腳本下載完成並准備妥善的情況。
IE 會發出一個readystatechange事件。<script>元素有一個readyState屬性,它的值隨着外部下載的過程而改變。readyState有5種取值。
uninitialized 默認狀態
loading 開始下載
interactive 下載完成但尚不可用
complete 所有數據都已經准備好
下面封裝一個函數來實現JavaScript文件的動態加載:
1 function loadScript (url, callback){ 2 var script = document.createElement ("script") 3 script.type = "text/javascript"; 4 if (script.readyState){ //IE 5 script.onreadystatechange = function(){ 6 if (script.readyState == "loaded" || script.readyState == "complete"){ 7 script.onreadystatechange = null; 8 callback(); 9 } 10 }; 11 } else { //Others 12 script.onload = function(){ 13 callback(); 14 }; 15 } 16 script.src = url; 17 document.getElementsByTagName_r("head")[0].appendChild(script); 18 }
使用方法:
1 loadScript("file1.js", function(){ 2 alert("File is loaded!"); 3 });
使文件按順序加載:
1 loadScript("file1.js", function(){ 2 loadScript("file2.js", function(){ 3 loadScript("file3.js", function(){ 4 alert("All files are loaded!"); 5 }); 6 }); 7 });
3.XHR腳本注入
使用XMLHttpRequest(XHR)對象將腳本注入到頁面中。此技術首先創建一個XHR對象,然后下載javas文件,接着用一個動態<script>元素將javas代碼注入頁面。
1 var xhr = new XMLHttpRequest(); 2 xhr.open("get", "file1.js", true); 3 xhr.onreadystatechange = function(){ 4 if (xhr.readyState == 4){ 5 if (xhr.status >= 200 && xhr.status < 300 | | xhr.status == 304){ 6 var script = document.createElement ("script"); 7 script.type = "text/javascript"; 8 script.text = xhr.responseText; 9 document.body.appendChild(script); 10 } 11 } 12 }; 13 xhr.send(null);
此代碼向服務器發送一個獲取file1.js文件的GET請求。onreadystatechange事件處理函數檢查readyState是不是4,然后檢查HTTP狀態碼是不是有效(2XX表示有效回應,304表示一個緩存響應)。如果收到一個有效的響應,那么就創建一個新的<script>元素,將它的文本屬性設置為從服務器接受到的resposeText字符串。這樣做實際上會創建一個帶有內聯代碼的<script>元素。一旦新的<script>元素被添加到文檔,代碼將被執行並准備使用。
這種方法的主要優點是,您可以下載不立即執行的 JavaScript 代碼。由於代碼返回在<script>
標簽之外(換句話說不受<script>
標簽約束),它下載后不會自動執行,這使得您可以推遲執行,直到一切都准備好了。另一個優點是,同樣的代碼在所有現代瀏覽器中都不會引發異常。
此方法最主要的限制是:JavaScript 文件必須與頁面放置在同一個域內,不能從 CDN 下載(CDN 指”內容投遞網絡(Content Delivery Network)”,所以大型網頁通常不采用 XHR 腳本注入技術。
減少 JavaScript 對性能的影響有以下幾種方法:
- 將所有的
<script>
標簽放到頁面底部,也就是</body>
閉合標簽之前,這能確保在腳本執行前頁面已經完成了渲染。 - 盡可能地合並腳本。頁面中的
<script>
標簽越少,加載也就越快,響應也越迅速。無論是外鏈腳本還是內嵌腳本都是如此。 - 采用無阻塞下載 JavaScript 腳本的方法:
- 使用
<script>
標簽的 defer 屬性(僅適用於 IE 和 Firefox 3.5 以上版本); - 使用動態創建的
<script>
元素來下載並執行代碼; - 使用 XHR 對象下載 JavaScript 代碼並注入頁面中。