Javascript性能優化之異步加載和執行

隨着科技的發展,如今的網站和五六年前相比,現在的人們對web的要求越來越高了,用戶體驗,交互效果,視覺效果等等都有很高的要求,要實現這些功能我們最就需要用到javascript,這時候JS的性能優化就越來越重要了,今天在這裏給大家轉載一篇IBM開發人員寫的怎麼優化javascript文章,我覺得寫的挺好,希望對大家有所幫助。


無論當前JavaScript代碼是內嵌還是在外鏈文件中,頁面的下載和渲染都必須停下來等待腳本執行完成。JavaScript執行過程耗時越久,瀏覽器等待響應用戶輸入的時間就越長。瀏覽器在下載和執行腳本時出現阻塞的原因在於,腳本可能會改變頁面或JavaScript的命名空間,它們對後面頁面內容造成影響。一個典型的例子就是在頁面中使用document.write()。


JavaScript 代碼內嵌示例

  1. <html>
  2. <head>
  3.     <title>Source Example</title>
  4. </head>
  5. <body>
  6.     <p>
  7.     <script type="text/javascript">
  8.         document.write("Today is " + (new Date()).toDateString());
  9.     </script>
  10.     </p>
  11. </body>
  12. </html>

當瀏覽器遇到script標籤時,當前 HTML 頁面無從獲知 JavaScript 是否會向<p> 標籤添加內容,或引入其他元素,或甚至移除該標籤。因此,這時瀏覽器會停止處理頁面,先執行 JavaScript代碼,然後再繼續解析和渲染頁面。同樣的情況也發生在使用 src 屬性加載 JavaScript的過程中,瀏覽器必須先花時間下載外鏈文件中的代碼,然後解析並執行它。在這個過程中,頁面渲染和用戶交互完全被阻塞了。


腳本位置

HTML 4 規範指出 script 標籤可以放在 HTML 文檔的head或body中,並允許出現多次。Web 開發人員一般習慣在 head 中加載外鏈的 JavaScript,接着用 <link> 標籤用來加載外鏈的 CSS 文件或者其他頁面信息。

  1. <html>
  2. <head>
  3.     <title>Source Example</title>
  4.     <script type="text/javascript" src="script1.js"></script>
  5.     <script type="text/javascript" src="script2.js"></script>
  6.     <script type="text/javascript" src="script3.js"></script>
  7.     <link rel="stylesheet" type="text/css" href="styles.css">
  8. </head>
  9. <body>
  10.     <p>Hello world!</p>
  11. </body>
  12. </html>

然而這種常規的做法卻隱藏着嚴重的性能問題。在清單 2 的示例中,當瀏覽器解析到 script 標籤(第 4 行)時,瀏覽器會停止解析其後的內容,而優先下載腳本文件,並執行其中的代碼,這意味着,其後的 styles.css 樣式文件和body標籤都無法被加載,由於body標籤無法被加載,那麼頁面自然就無法渲染了。因此在該 JavaScript 代碼完全執行完之前,頁面都是一片空白。

圖 1 描述了頁面加載過程中腳本和樣式文件的下載過程。    

從 IE 8、Firefox 3.5、Safari 4 和 Chrome 2 開始都允許並行下載 JavaScript 文件。這是個好消息,因爲script標籤在下載外部資源時不會阻塞其他script標籤。遺憾的是,JavaScript 下載過程仍然會阻塞其他資源的下載,比如樣式文件和圖片。儘管腳本的下載過程不會互相影響,但頁面仍然必須等待所有 JavaScript 代碼下載並執行完成才能繼續。因此,儘管最新的瀏覽器通過允許並行下載提高了性能,但問題尚未完全解決,腳本阻塞仍然是一個問題。


我們可以發現一個有趣的現象:第一個 JavaScript 文件開始下載,與此同時阻塞了頁面其他文件的下載。此外,從 script1.js 下載完成到 script2.js 開始下載前存在一個延時,這段時間正好是 script1.js 文件的執行過程。每個文件必須等到前一個文件下載並執行完成纔會開始下載。在這些文件逐個下載過程中,用戶看到的是一片空白的頁面。


由於腳本會阻塞頁面其他資源的下載,因此推薦將所有script標籤儘可能放到body標籤的底部,以儘量減少對整個頁面下載的影響。請看下面推薦的代碼放置位置示例

  1. <html>
  2. <head>
  3.     <title>Source Example</title>
  4.     <link rel="stylesheet" type="text/css" href="styles.css">
  5. </head>
  6. <body>
  7.     <p>Hello world!</p>
  8.     <!-- Example of efficient script positioning -->
  9.     <script type="text/javascript" src="script1.js"></script>
  10.     <script type="text/javascript" src="script2.js"></script>
  11.     <script type="text/javascript" src="script3.js"></script>
  12. </body>
  13. </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事件觸發後再下載腳本。有多種方式可以實現這一效果。


延遲加載腳本

HTML 4 爲script標籤定義了一個擴展屬性:defer。Defer 屬性指明本元素所含的腳本不會修改 DOM,因此代碼能安全地延遲執行。defer 屬性只被 IE 4 和 Firefox 3.5 更高版本的瀏覽器所支持,所以它不是一個理想的跨瀏覽器解決方案。在其他瀏覽器中,defer 屬性會被直接忽略,因此script標籤會以默認的方式處理,也就是說會造成阻塞。然而,如果您的目標瀏覽器支持的話,這仍然是個有用的解決方案。請看代碼:

  1. <script type="text/javascript" src="script1.js" defer></script>

帶有 defer 屬性的script標籤可以放置在文檔的任何位置。對應的 JavaScript 文件將在頁面解析到script標籤時開始下載,但不會執行,直到 DOM 加載完成,即onload事件觸發前纔會被執行。當一個帶有 defer 屬性的 JavaScript 文件下載時,它不會阻塞瀏覽器的其他進程,因此這類文件可以與其他資源文件一起並行下載。


任何帶有 defer 屬性的script元素在 DOM 完成加載之前都不會被執行,無論內嵌或者是外鏈腳本都是如此。清單 5 的例子展示了defer屬性如何影響腳本行爲:

  1. <html>
  2. <head>
  3.     <title>Script Defer Example</title>
  4. </head>
  5. <body>
  6.     <script type="text/javascript" defer>
  7.         alert("defer");
  8.     </script>
  9.     <script type="text/javascript">
  10.         alert("script");
  11.     </script>
  12.     <script type="text/javascript">
  13.         window.onload = function(){
  14.             alert("load");
  15.         };
  16.     </script>
  17. </body>
  18. </html>

這段代碼在頁面處理過程中彈出三次對話框。不支持 defer 屬性的瀏覽器的彈出順序是:“defer”、“script”、“load”。而在支持 defer 屬性的瀏覽器上,彈出的順序則是:“script”、“defer”、“load”。請注意,帶有 defer 屬性的script元素不是跟在第二個後面執行,而是在 onload 事件被觸發前被調用。


如果您的目標瀏覽器只包括 Internet Explorer 和 Firefox 3.5,那麼 defer 腳本確實有用。如果您需要支持跨領域的多種瀏覽器,那麼還有更一致的實現方式。


HTML 5 爲script標籤定義了一個新的擴展屬性:async。它的作用和 defer 一樣,能夠異步地加載和執行腳本,不因爲加載腳本而阻塞頁面的加載。但是有一點需要注意,在有 async 的情況下,JavaScript 腳本一旦下載好了就會執行,所以很有可能不是按照原本的順序來執行的。如果 JavaScript 腳本前後有依賴性,使用 async 就很有可能出現錯誤。


動態腳本元素

文檔對象模型(DOM)允許您使用 JavaScript 動態創建 HTML 的幾乎全部文檔內容。script元素與頁面其他元素一樣,可以非常容易地通過標準 DOM 函數創建:

  1. var script = document.createElement ("script");
  2.    script.type = "text/javascript";
  3.    script.src = "script1.js";
  4.    document.getElementsByTagName("head")[0].appendChild(script);

新的script元素加載 script1.js 源文件。此文件當元素添加到頁面之後立刻開始下載。此技術的重點在於:無論在何處啓動下載,文件的下載和運行都不會阻塞其他頁面處理過程。您甚至可以將這些代碼放在head部分而不會對其餘部分的頁面代碼造成影響(除了用於下載文件的 HTTP 連接)。


當文件使用動態腳本節點下載時,返回的代碼通常立即執行(除了 Firefox 和 Opera,他們將等待此前的所有動態腳本節點執行完畢)。當腳本是“自運行”類型時,這一機制運行正常,但是如果腳本只包含供頁面其他腳本調用調用的接口,則會帶來問題。這種情況下,您需要跟蹤腳本下載完成並是否準備妥善。可以使用動態 script 節點發出事件得到相關信息。


Firefox、Opera, Chorme 和 Safari 3+會在script節點接收完成之後發出一個 onload 事件。您可以監聽這一事件,以得到腳本準備好的通知:

  1. var script = document.createElement ("script")
  2. script.type = "text/javascript";
  3. //Firefox, Opera, Chrome, Safari 3+
  4. script.onload = function(){
  5.     alert("Script loaded!");
  6. };
  7. script.src = "script1.js";
  8. 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 事件句柄(保證事件不會被處理兩次):


通過檢查 readyState 狀態加載 JavaScript 腳本:

  1. var script = document.createElement("script")
  2. script.type = "text/javascript";
  3. //Internet Explorer
  4. script.onreadystatechange = function(){
  5.      if (script.readyState == "loaded" || script.readyState == "complete"){
  6.            script.onreadystatechange = null;
  7.            alert("Script loaded.");
  8.      }
  9. };
  10. script.src = "script1.js";
  11. document.getElementsByTagName("head")[0].appendChild(script);


大多數情況下,您希望調用一個函數就可以實現 JavaScript 文件的動態加載。下面的函數封裝了標準實現和 IE 實現所需的功能:

通過函數進行封裝:

  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("head")[0].appendChild(script);
  18. }

此函數接收兩個參數:JavaScript 文件的 URL,和一個當 JavaScript 接收完成時觸發的回調函數。屬性檢查用於決定監視哪種事件。最後一步,設置 src 屬性,並將script元素添加至頁面。此 loadScript() 函數使用方法如下:

  1. loadScript("script1.js", function(){
  2.     alert("File is loaded!");
  3. });


您可以在頁面中動態加載很多 JavaScript 文件,但要注意,瀏覽器不保證文件加載的順序。所有主流瀏覽器之中,只有 Firefox 和 Opera 保證腳本按照您指定的順序執行。其他瀏覽器將按照服務器返回它們的次序下載並運行不同的代碼文件。您可以將下載操作串聯在一起以保證他們的次序,如下:

  1. loadScript("script1.js", function(){
  2.     loadScript("script2.js", function(){
  3.         loadScript("script3.js", function(){
  4.             alert("All files are loaded!");
  5.         });
  6.     });
  7. });

此代碼等待 script1.js 可用之後纔開始加載 script2.js,等 script2.js 可用之後纔開始加載 script3.js。雖然此方法可行,但如果要下載和執行的文件很多,還是有些麻煩。如果多個文件的次序十分重要,更好的辦法是將這些文件按照正確的次序連接成一個文件。獨立文件可以一次性下載所有代碼(由於這是異步進行的,使用一個大文件並沒有什麼損失)。


動態腳本加載是非阻塞 JavaScript 下載中最常用的模式,因爲它可以跨瀏覽器,而且簡單易用。


使用 XMLHttpRequest(XHR)對象

此技術首先創建一個 XHR 對象,然後下載 JavaScript 文件,接着用一個動態 script 元素將 JavaScript 代碼注入頁面。

通過 XHR 對象加載 JavaScript 腳本:

  1. var xhr = new XMLHttpRequest();
  2. xhr.open("get", "script1.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);

此代碼向服務器發送一個獲取 script1.js 文件的 GET 請求。onreadystatechange 事件處理函數檢查 readyState 是不是 4,然後檢查 HTTP 狀態碼是不是有效(2XX 表示有效的迴應,304 表示一個緩存響應)。如果收到了一個有效的響應,那麼就創建一個新的script元素,將它的文本屬性設置爲從服務器接收到的 responseText 字符串。這樣做實際上會創建一個帶有內聯代碼的script元素。一旦新script元素被添加到文檔,代碼將被執行,並準備使用。


這種方法的主要優點是,您可以下載不立即執行的 JavaScript 代碼。由於代碼返回在script標籤之外(換句話說不受script標籤約束),它下載後不會自動執行,這使得您可以推遲執行,直到一切都準備好了。另一個優點是,同樣的代碼在所有現代瀏覽器中都不會引發異常。


此方法最主要的限制是:JavaScript 文件必須與頁面放置在同一個域內,不能從 CDN 下載(CDN 指"內容投遞網絡(Content Delivery Network)",所以大型網頁通常不採用 XHR 腳本注入技術。

總結

減少 JavaScript 對性能的影響有以下幾種方法:


1.將所有的


2.儘可能地合併腳本。頁面中的


3.採用無阻塞下載 JavaScript 腳本的方法:

    3.1.使用

    3.2.使用動態創建的

    3.3.使用 XHR 對象下載 JavaScript 代碼並注入頁面中。

    3.4.通過以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 網站和應用的實際性能。


轉載自原文:IBM DeveloperWorks

javas
javasc
javascr
javascri
javascrip
javascript
javascrip
javascri
javascrip
javascript
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章