談談異步加載JavaScript

前言javascript

關於JavaScript腳本加載的問題,相信你們碰到不少。主要在幾個點——html

1> 同步腳本和異步腳本帶來的文件加載、文件依賴及執行順序問題
2> 同步腳本和異步腳本帶來的性能優化問題

深刻理解腳本加載相關的方方面面問題,不只利於解決實際問題,更加利於對性能優化的把握並執行。java

 

先看隨便一個script標籤代碼——ajax

<script src="js/myApp.js"></script>

若是放在<head>上面,會阻塞全部頁面渲染工做,使得用戶在腳本加載完畢並執行完畢以前一直處於「白屏死機」狀態。而<body>末尾的打腳本只會讓用戶看到毫無生命力的靜態頁面,本來應該進行客戶端渲染的地方卻散佈着不起做用的控件和空空如也的方框。拿一個測試用例——編程

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>異步加載script</title>
    <script src="js/test.js"></script>
</head>
<body>
    <div>我是內容</div>
    <img src="img/test.jpg">
</body>
</html>

其中,test.js中的內容——瀏覽器

alert('我是head裏面的腳本代碼,執行這裏的js以後,纔開始進行body的內容渲染!');

咱們會看到,alert是一個暫停點,此時,頁面是空白的。可是要注意,此時整個頁面已經加載完畢,若是body中包含某些src屬性的標籤(如上面的img標籤),此時瀏覽器已經開始加載相關內容了。總之要注意——js引擎和渲染引擎的工做時機是互斥的(一些書上叫它爲UI線程)。性能優化

所以,咱們須要——那些負責讓頁面更好看、更好用的腳本應該當即加載,而那些能夠待會兒再加載的腳本稍後再加載。服務器

 

1、腳本延遲執行app

如今愈來愈流行把腳本放在頁面<body>標籤的尾部。這樣,一方面用戶能夠更快地看到頁面,另外一方面腳本能夠直接操做已經加載完成的dom元素。對於大多數腳本而言,此次「搬家」是個巨大的進步。該頁面模型以下——dom

<!DOCTYPE html>
<html>
<head lang="en">
    <!--metadata and scriptsheets go here-->
    <script src="headScript.js"></script>
</head>
<body>
    <!--content goes here-->
    <script src="bodyScript.js"></script>
</body>
</html>

這確實大大加快了頁面的渲染時間,可是注意一點,這可能讓用戶有機會在加載bodyScript以前與頁面交互。源於瀏覽器在加載完整個文檔以前沒法加載這些腳本,這對那些經過慢速鏈接傳送的大型文檔來講會是一大瓶頸。

理想狀況下,腳本的加載應該與文檔的加載同時進行,而且不影響DOM的渲染。這樣,一旦文檔就緒就能夠運行腳本,由於已經按照<script>標籤的次序加載了相應腳本。

咱們使用defer便可以完成這樣的需求,即——

<script src="deferredScript.js"></script>

添加defer屬性至關於告訴瀏覽器:請立刻開始加載這個腳本吧,可是,請等到文檔就緒且此前全部具備defer屬性的腳本都結束運行以後再運行它。

這樣,在head標籤裏放入延遲腳本,技能帶來腳本置於body標籤時的全部好處,又能讓大文檔的加載速度大幅提高。此時的頁面模式即是——

<!DOCTYPE html>
<html>
<head lang="en">
    <!--metadata and scriptsheets go here-->
    <script src="headScript.js"></script>
    <script src="deferredScript.js" defer></script>
</head>
<body>
    <!--content goes here-->
</body>
</html>

可是並不是全部的瀏覽器都支持defer(對於一些modern瀏覽器,若是聲明defer,其內部腳本將不會執行document.write及DOM渲染操做。IE4+均支持defer屬性)。這意味着,若是想確保本身的延遲腳本能在文檔加載後運行,就必須將全部延遲腳本的代碼都封裝在諸如jQuery之$(document).ready之類的結構中。這是值得的,由於差很少97%的訪客都能享受到並行加載的好處,同時另外3%的訪客仍然能使用功能完整的JavaScript。

 

2、腳本的徹底並行化

讓腳本的加載及執行再快一步,我不想等到defer腳本一個接着一個運行(defer讓咱們想到一種靜靜等待文檔加載的有序排隊場景),更不想等到文檔就緒以後才運行這些腳本,我想要儘快加載而且儘快運行這些腳本。這裏也就想到了HTML5的async屬性,可是要注意,它是一種混亂的無政府狀態。

例如,咱們加載兩個徹底不相干的第三方腳本,頁面沒有它們也運行得很好,並且也不在意它們誰先運行誰後運行。所以,對這些第三方腳本使用async屬性,至關於一分錢沒花就提高了它們的運行速度。

async屬性是HTML5新增的。做用和defer相似,即容許在下載腳本的同時進行DOM的渲染。可是它將在下載後儘快執行(即JS引擎空閒了立馬執行),不能保證腳本會按順序執行。它們將在onload 事件以前完成。 

Firefox 3.六、Opera 10.五、IE 9 和 最新的Chrome 和 Safari 都支持 async 屬性。能夠同時使用 async 和 defer,這樣IE 4以後的全部 IE 都支持異步加載,可是要注意,async會覆蓋掉defer。

那麼此時的頁面模型以下——

<!DOCTYPE html>
<html>
<head lang="en">
    <!--metadata and scriptsheets go here-->
    <script src="headScript.js"></script>
    <script src="deferredScript.js" defer></script>
</head>
<body>
    <!--content goes here-->
    <script src="asyncScript1.js" async defer></script>
    <script src="asyncScript2.js" async defer></script>
</body>
</html>

要注意這裏的執行順序——各個腳本文件加載,接着執行headScript.js,緊接着在DOM渲染的同時會在後臺加載defferedScript.js。接着在DOM渲染結束時將運行defferedScript.js和那兩個異步腳本,要注意對於支持async屬性的瀏覽器而言,這兩個腳本將作無序運行。

 

3、可編程的腳本加載

儘管上面兩個腳本屬性的功能很是吸引人,可是因爲兼容性的問題,應用並非很普遍。故此,咱們更多使用腳本加載其餘腳本。例如,咱們只想給那些知足必定條件的用戶加載某個腳本,也就是常常提到的「懶加載」。

在瀏覽器API層面,有兩種合理的方法來抓取並運行服務器腳本——

1> 生成ajax請求並用eval函數處理響應

2> 向DOM插入<script>標籤

後一種方式更好,由於瀏覽器會替咱們操心生成HTTP請求這樣的事。再者,eval也有一些實際問題:泄露做用域,調試搞得一團糟,並且還可能下降性能。所以,想要加載名爲feture.js的腳本,咱們應該使用相似下面的代碼:

var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.src = 'feature.js';
head.appendChild(script);

固然,咱們要處理回調監聽,HTML5規範定義了一個能夠綁定回調的onload屬性。

script.onload = function() {
    console.log('script loaded ...');
}

不過,IE8及更老的版本並不支持onload,它們支持的是onreadystatechange。並且,對於錯誤處理仍然千奇百怪。在這裏,能夠多參考一些流行的校本加載庫,如labjs、yepnope、requirejs等。

以下,本身封裝了一個簡易loadjs文件——

var loadJS = function(url,callback){
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.src = url;
    script.type = "text/javascript";
    head.appendChild( script);

    // script 標籤,IE下有onreadystatechange事件, w3c標準有onload事件
    // IE9+也支持 W3C標準的onload
    var ua = navigator.userAgent,
        ua_version;
    // IE6/7/8
    if (/MSIE ([^;]+)/.test(ua)) {
        ua_version = parseFloat(RegExp["$1"], 10);
        if (ua_version <= 8) {
            script.onreadystatechange = function(){
                if (this.readyState == "loaded" ){
                    callback();
                }
            }
        } else {
            script.onload = function(){
                callback();
            };
        }
    } else {
        script.onload = function(){
            callback();
        };
    }
};

對於document.write的方式異步加載腳本,在這裏就不說了,如今不多有人這麼幹了,由於瀏覽器差別性實在是搞得頭大。

要注意,使用 Image 對象異步預加載 js 文件,裏面的js代碼將不會被執行。

最後,談一下requirejs中的異步加載腳本。

requirejs不會保證按順序運行目標腳本,只是保證它們的運行次序能知足各自的依賴性要求。從而咱們確保了儘快的並行加載全部腳本,並有條不紊的按照依賴性拓撲結構去執行這些腳本。

 

4、總結

OK,談到這兒,異步加載腳本的陳述也就完了。我再次囉嗦一下這裏的優化順序——

1> 傳統的方式,咱們使用script標籤直接嵌入到html文檔中,這裏分兩種狀況——

  a> 嵌入到head標籤中——要注意,這樣作並不會影響文檔內容中其餘靜態資源文件的並行加載,它影響的是,文檔內容的渲染,即此時的DOM渲染就會被阻塞,呈現白屏。

  b> 嵌入到body標籤底部——爲了免去白屏現象,咱們優先進行DOM的渲染,再去執行腳本,但問題又來了。先說第一個問題——若是DOM文檔內容比較大,交互事件綁定便有了延遲,體驗便差了些。固然,咱們須要根據需求而定,讓重要的腳本優先執行。再說第二個問題——因爲腳本文件至於body底部,致使對於這些腳本的加載相對於至於head中的腳本而言,它們的加載便有了延遲。因此,至於body底部,也並不是是優化的終點。

  c> 添加defer屬性——咱們但願腳本儘早的進行並行加載,咱們把這批腳本依舊放入head中。腳本的加載應該與文檔的加載同時進行,而且不影響DOM的渲染。這樣,一旦文檔就緒就能夠運行腳本。因此便有了defer這樣屬性。可是要注意它的兼容性,對於不支持defer屬性的瀏覽器,咱們須要將代碼封裝在諸如jQuery之$(document).ready中。須要注意一點,全部的defer屬性的腳本,是按照其出場順序依次執行,所以,它一樣嚴格同步。

 2> 上一點,講的都是同步執行腳本(要注意,這些腳本的加載過程是並行的,只不過是誰先觸發請求誰後觸發請求的區別而已),接下來的優化點即是「並行執行腳本」,固然,咱們知道,一個時間點,只有執行一個js文件,這裏的「並行」是指,誰先加載完了,只要此時js引擎空閒,立馬執行之。這裏的優化分紅兩種——

  a> 添加async這個屬性——確實可以完成上面咱們所說的優化點,可是它有很高的侷限性,即僅僅是針對非依賴性腳本加載,最恰當的例子即是引入多個第三方腳本了。還有就是與deffer屬性的合用,實在是讓人大費腦筋。固然,它也存在兼容性問題。以上三個問題便致使其應用並不普遍。當使用async的時候,必定要嚴格注意依賴性問題。

  b> 腳本加載腳本——很顯然,咱們使用之來達到「並行執行腳本」的目的。同時,咱們也方便去控制腳本依賴的問題,咱們便使用瞭如requirejs中對於js異步加載的智能化加載管理。

好,寫到這兒。

這裏,我僅僅談的是異步加載腳本的相關內容。還有一起內容,即是異步加載樣式文件或者其餘靜態資源。待續......

 

參考:

  《JavaScript異步編程》

相關文章
相關標籤/搜索