javascript腳本阻塞與模塊化加載

 

javascript腳本執行過程中會中斷頁面加載,直到腳本執行完畢,此操作阻塞了頁面加載,造成性能問題。

——《高性能javascript》

 

腳本位置和加載順序:

如果將腳本放在head內,那麼再腳本執行完畢之前,顯示給用戶的始終是一片空白,用戶只能傻傻的看着屏幕等待腳本執行完畢。

而且,如果頁面引入多個腳本,那麼後面的腳本文件必須等待前面的腳本文件下載完畢並且執行完畢之後才能開始下載並運行。不過IE8,FF,SAFARI,CHROME已經允許腳本文件可以同時下載,不過儘管如此,javascript腳本仍然會阻塞其他腳本下載進程,頁面仍舊要等待所有javascript腳本下載並執行完畢之後纔可以開始加載渲染。

因此,儘可能的將腳本文件放置在body標籤的底部,以減少腳本阻塞對頁面性能的影響,這也是Yahoo性能優化的第一條定律。

成組腳本加載:

我們知道較少HTTP請求數可以有效提高頁面加載速度,澤卡斯說:“下載一個100KB的JS文件要比下載4個25KB的JS文件速度快”。畢竟有請求和響應的時間。所以我們可以將多個JS文件打包壓縮成一個來提升性能。YUI通過CDN網絡將客戶端請求的多個JS文件在服務器端合併並壓縮成一個返回給客戶端,從而提高加載效率。例如:

<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> 通過這個請求就可以把min.js和event-min.js兩個文件在服務器端合併壓縮成一個返回。

非阻塞腳本和延遲腳本(Deferred scripts):

雖然將多個javascript腳本合併並且將腳本放在body底部降低了HTTP請求數並且部分解決了阻塞問題。但如果腳本文件很大, 而且在每個腳本中都有功能函數運行,那麼在腳本文件加載時,會佔用瀏覽器很長一段時間,這段時間用戶也只能傻傻的看着屏幕玩弄着沒有任何反應的瀏覽器。爲了避開這種情況,就出現了模塊化加載和按需加載。

HTML4爲<script>標籤擴展了defer屬性,設置過該屬性的script腳本可以放在頁面的任何位置,並且不會阻塞頁面其他資源的下載,也就是說可以實現頁面內容的並行加載。但下載完成後代碼不會執行,只有等到DOM完全加載完畢後onload事件發生之前被執行。在《高性能JS》中,作者提到只有IE4和FF3.5的更高版本支持,不過在這篇文章中說webkit內核在HTML5也已經支持deffer和async。考慮到瀏覽器兼容性和更加強大和靈活的腳本控制,我們就需要引入按需加載。

我們可以通過動態創建script標記,更改其屬性,並添加至head內,完成對script加載順序、時間、依賴關係的控制。

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

這樣做的好處在於:無論在什麼地方加載file1.js,都不會阻塞和影響頁面其他內容的加載,當然,會影響HTTP請求。一般情況下,通過動態節點下載腳本文件時,file1.js被加載完畢之後,往往會立即執行(除了FF和Opera,他們會等待此前的所有動態節點腳本執行完畢)。當腳本文件是“自執行(function(){})()”時,一切都很正常,但若只是一般函數命名定義或者只是提供了相關接口,就會有一些問題(至於什麼問題,我沒有驗證,但估計是腳本加載程度的問題,書中只說在這種情況下需要跟蹤腳本下載完成並準備妥善的情況.若你有相關資料或者見解,非常希望能給出,並給予指點。)。針對這一情況,《高性能JS》中給出了一些解決方法:FF,Chrome,Safari,Opera會在<script>節點腳本接收完成後發出一個load事件,IE會給出readystatechanage事件(script元素有一個readyState屬性,它的值隨文件的下載狀態而改變,共有5中取值,不在一一列出,我們在這裏使用loaded和complete來表示下載完成和所有數據已經準備好)。我們可以通過這兩個事件判斷腳本是否加載完畢,下面是文中提供的一個封裝好的函數,兼容各主流瀏覽器:

   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:  }

兩個參數@url:javascript文件URL、@callback:javascript接收完成時的回調函數,最後設置src屬性,將<script>元素添加至頁面。這裏爲什麼不是先設置src,然後才判斷script加載情況呢?如果先設置了,還判斷幹毛?

如果我們要按順序和依賴關係加載多個javascript腳本文件,瀏覽器在此時並不保證文件執行順序。所有瀏覽器中只有FF和Opera保證腳本按照下載順序執行,其他瀏覽器將按照服務器返回的順序加載運行。那麼我們就需要運用上面的例子在回調函數中按順序加載執行腳本。

   1:  loadScript("file1.js", function(){
   2:      loadScript("file2.js", function(){
   3:          loadScript("file3.js", function(){
   4:              alert("All files are loaded!");
   5:          });
   6:      });
   7:  });

不過這樣明顯很麻煩,而且在速度上也是個問題。還有一種非阻塞腳本的方法是XHR腳本注入,也是異步加載,不會影響頁面其他內容的加載進程。

推薦的腳本依賴和加載模式:

在多個javascript腳本之間存在依賴關係時,必須將依賴性最小的放在最前面,將依賴性最大的放在最後面。若加上我們剛纔的腳本阻塞和異步加載問題,下面給出可行性較高的解決方案:

第一步:先向頁面中加入“動態引入腳本(就像上面的loadScript)”的函數或庫文件,因爲這部分代碼可能很少,所以下載運行非常迅速,不會對頁面性能造成很大幹擾。

第二步:按需動態加載其他模塊所需的腳本代碼。

例如:

   1:  <script type="text/javascript" src="loader.js"></script>
   2:  <script type="text/javascript">
   3:      loadScript("the-rest.js", function(){
   4:          Application.init();
   5:      });
   6:  </script>

記得將此代碼放在</body>標記之前。這樣不僅可以保證腳本不會影響頁面其他內容,而且也不需要用額外的window.onload事件做判斷。我們甚至可以將loader.js的內容直接放在頁面中,這樣可以減少一次http請求。

可以參考和使用的案例:

YUI 3:

YUI 3的核心設計理念爲:用一段很小的初始代碼,下載其他的功能模塊代碼。

例子:

   1:  //引入YUI 3
   2:  <script type="text/javascript"src="http://yui.yahooapis.com/combo?3.0.0/build/yui/yui-min.js"></script>

3: //如果想使用dom功能,就可以給出此功能的名字,傳遞給use函數,並給出一個回調函數,當dom模塊加載完畢後,就會執行回調函數中的內容,

回調函數中的參數Y代表了YUI實例,我們可以在回調函數中使用剛加載完成的dom模塊中的功能。實際上,在加載dom模塊之前,如果dom模塊依賴其他爲

加載的模塊,當我們在use函數中指定過dom參數後,YUI會自動創建一個加載dom模塊所需要的所有依賴模塊,並創建一個“聯合句柄URL”,(毛的聯合

句柄URL,實際上就是把所需的腳本模塊的url寫在一個URL上,然後通過CDN合併壓縮),然後按順序下載和執行所需模塊。

   4:  YUI().use("dom", function(Y){
   5:      Y.DOM.addClass(docment.body, "loaded");
   6:  });
 

LazyLoad:

Yahoo!Search的Ryan Grove 創建了LazyLoad庫,精縮之後只有1.5K。

例子:

   1:  <script type="text/javascript" src="lazyload-min.js"></script>
   2:  <script type="text/javascript">
   3:      LazyLoad.js("the-rest.js", function(){
   4:          Application.init();
   5:      });
   6:  </script>
   7:   
   8:  //若需要加載多個腳本文件,並保證執行順序,可以這樣:
   9:  <script type="text/javascript" src="lazyload-min.js"></script>
  10:  <script type="text/javascript">
  11:      LazyLoad.js(["first-file.js", "the-rest.js"], function(){
  12:          Application.init();
  13:      });
  14:  </script> 

雖然非阻塞動態加載,但儘量減少文件數量,因爲每一次下載仍是一個單獨的HTTP請求,回調函數直到所有文件下載完畢之後纔會執行。

 

SeaJS:(其實我覺得應該把這個放在最前面)

國內淘寶達人玉伯的SeaJs,據用過的人說比前面幾種都要好用,但自己沒有研究過,所以請童鞋們多多發表自己的見解,多多指教。。。

例子:

   1:  <script src="sea.js"></script>
   2:  <script>
   3:    seajs.use('./example', function(example) {
   4:      example.sayHello();
   5:    });
   6:  </script>

下面給出一些SeaJS的參考和使用資料:

  1. SeaJs首頁:http://seajs.com/
  2. 使用SeaJS實現模塊化開發:http://cnodejs.org/blog/?p=1203
  3. 歲月如歌在javaeye上SeaJS1.0正式發佈的博文:http://www.iteye.com/topic/1112630

 

LABjs:

Kyle Simpson寫的開源庫LABjs,精縮後4.5K,據說對並行下載和精確控制依賴關係更有針對性。

   1:  <script type="text/javascript" src="lab.js"></script>
   2:  <script type="text/javascript">
   3:      $LAB.script("the-rest.js")
   4:          .wait(function(){
   5:              Application.init();
   6:          });
   7:  </script>
   8:   
   9:  //LAB支持鏈式操作,每個函數默認返回一個$LAB對象的引用,要加載多個腳本,可以這樣:
  10:  <script type="text/javascript" src="lab.js"></script>
  11:  <script type="text/javascript">
  12:      $LAB.script("first-file.js")
  13:          .script("the-rest.js")
  14:          .wait(function(){
  15:              Application.init();
  16:          });
  17:  </script>
  18:   
  19:  //如果想管理依賴關係,可以通過wait函數,這樣:
  20:  <script type="text/javascript" src="lab.js"></script>
  21:  <script type="text/javascript">
  22:      $LAB.script("first-file.js").wait()
  23:          .script("the-rest.js")
  24:          .wait(function(){
  25:              Application.init();
  26:          });
  27:  </script>

此時,雖然文件是並行下載,但first-file.js一定會在the-rest.js之前執行。

 

RequireJS:

jrburke 的 RequireJS

例子:

   1:  <script data-main="scripts/main" src="scripts/require.js"></script>
   2:  require(["helper/util"], function() {
   3:      //This function is called when scripts/helper/util.js is loaded.
   4:  });
   5:   
   6:  //加載多個JS:
   7:  require(["helper/util","helper/util1","helper/util2","helper/util3"], function() {
   8:      //This function is called when scripts/helper/util.js is loaded.
   9:  });

轉載請註明出處:http://www.cnblogs.com/mrsunny/archive/2011/10/22/2221343.html#

 

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