加載和執行JS的正確姿式

前言

kyrieliuの《高性能JavaScript》讀書筆記。javascript

script標籤是一個很「霸道」的狠角色,它的每次出現都讓頁面等待腳本的解析和執行。也就是說,無論當前的javascript代碼是內嵌仍是包含在外鏈文件中,頁面的下載和渲染都必須停下來等待腳本執行完成。
其實,script標籤的「霸道」是必須的,由於頁面的生存週期中,腳本的執行可能會修改頁面的內容。
總之,在解析和執行js的這個過程當中,頁面渲染和用戶交互徹底被阻塞了。css

腳本位置

提及腳本的位置,腦海裏不由想起來那兩句真言:html

  1. css放在<head>前端

  2. js放在</body>java

至於why呢,舉個慄砸:web

<html>
    <head>
        <title>一個栗子</title>
        <script type='text/javascript' src='js_file_1.js'></script>
        <script type='text/javascript' src='js_file_2.js'></script>
        <script type='text/javascript' src='js_file_3.js'></script>
        <link rel='stylesheet' type='text/css' href='css_file.css' />
    </head>
    
    <body>
        <p>hello from kyrieliu.</p>
    </body>
</html>

這一段看似正常的代碼實際上有着肥腸嚴重的性能問題:在head中加載了三個js文件。
前面我有說到,js文件會阻塞頁面渲染,知道它們所有下載並執行完畢後,頁面渲染才能繼續(無情無恥無理取鬧)。
縱觀一份html文檔,用戶真正看得見的內容基本上都寫在body標籤裏,也就是說,瀏覽器在解析到body標籤以前,不會渲染頁面的任何部分。如今又寫了三個腳本到head標籤裏面,好嘞,這下子渲染的延遲更明顯了,用戶在打開這樣的一個頁面時,看到了what?白屏!用戶不能瀏覽頁面的內容,更沒法與頁面進行交互。跨域

場景還原

第一個js文件開始下載,與此同時阻塞了頁面其餘文件的下載。等呀等,ok,第一個js文件終於下載完了,第二個js按捺不住本身喜悅的心情,正要開始下載,忽然,第一個js文件說:「且慢,老子尚未完事呢」,第二個js文件嚇了一跳,站在原地不動。第一個js文件開始執行,等到執行完畢,第二個js文件才得以開始下載。
總之,每一個文件必須等到前一個文件下載並執行完成纔會開始下載
在這些文件"one by one"的下載執行過程當中,用戶看到的則是一片空白。瀏覽器

不是那麼好的好消息

IE8/Firefox3.5/Safari 4/Chrome 2都容許並行下載js文件,也就是說,script標籤在下載外部資源時,不會阻塞其餘的script標籤。
遺憾的是:安全

  1. js文件的下載過程仍然會阻塞其餘資源的下載,好比圖片。微信

  2. 頁面仍然必須等到全部js代碼下載並執行完畢才能繼續渲染。

所以,儘管瀏覽器經過容許並行下載提升了性能,但腳本阻塞仍然是一個問題
綜上,推薦將全部的script標籤儘量的放在body標籤的底部,以儘可能減小對整個頁面渲染的影響。

組織腳本

既然每一個script標籤初始下載時都會阻塞頁面渲染,那麼咱們能夠經過減小頁面上script標籤的數量來改善這一狀況。不光是外鏈的腳本,內嵌腳本的數量一樣也要限制(畢竟執行js代碼也會阻塞頁面的渲染)。
多於外鏈的腳本,這裏的狀況有一點須要額外注意的地方:考慮HTTP請求會帶來額外的性能開銷,因此下載單個100kb的文件要比下載四個25kb的文件更快。從這個角度出發,更能說明減小外鏈腳本文件的數量將會改善性能。
What u should do?合併腳本!

無阻塞的腳本

隨着web應用的功能越豐富,所須要的js代碼就越多,因此精簡源代碼也並不老是可行。儘管下載單個較大的js文件只產生一次HTTP請求,但這樣作卻會鎖死瀏覽器一大段時間。
爲了不這種狀況,須要向頁面中逐步加載js文件,這樣作,在某種程度上來講不會阻塞瀏覽器。

延遲的腳本

HTML4爲script標籤訂義了一個擴展屬性:defer。Defer代表本元素所含的腳本不會修改DOM,所以代碼能安全的延遲執行
帶有defer屬性的script標籤能夠放在文檔的任何位置(不會阻塞瀏覽器的其餘進程,此類文件能夠與頁面中的其餘資源並行下載),對應的js代碼將在頁面解析到script標籤時開始下載,但並不會執行,(onload事件被出發前)纔會執行。
PS:截至這本書的初版(2010年11月),這個屬性對IE和Firefox的支持性比較好(個人天竟然有IE),若是真要投入到實際的項目中,不妨先去檢查一下瀏覽器的兼容性先~

動態腳本元素

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'js_file.js';
document.getElementsByTagName('body')[0].appendChild(script);

Firefox/Opera/Chrome和Safari 3+會在script元素接收完成時出發一個load事件,因此你能夠經過監聽這個事件來得到腳本加載完成時的狀態:

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

一貫特立獨行的IE天然有他的另外一套:觸發一個readystatechange事件。script元素提供一個readyState屬性,有如下五種取值:

  1. "uninitialized"

  2. "loading"

  3. "loaded"

  4. "interactive" 數據完成下載但尚不可用

  5. "complete"

因此,

script.onreadystatechange = function(){
    if (script.readyState == 'loaded' || script.readyState == 'complete'){
        script.onreadystatechange = null;
        console.log('script loaded.');
    }
}

至此,咱們獲得了一個能夠應用於普遍瀏覽器的動態加載腳本用的函數:

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{
        script.onload = function(){
            callback();
        }
    }
}

能夠這麼用:

loadScript('file.js',function(){
    console.log('script loaded.');
});

也能夠這麼用:

loadScript('file_1.js',function(){
    loadScript('file_2.js',function(){
        loadScript('file_3.js',function(){
            console.log('all files are loaded.');
        });
    });
});

XMLHttpRequest腳本注入

標題看起來很高大上的樣子,其實就是Ajax。

var xhr = new XMLHttpRequest();
xhr.open('get','file.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);

侷限:跨域問題。

小結

  • body閉合標籤以前,將全部的script標籤放到頁面底部。這樣能確保在腳本執行前頁面已經完成了渲染。

  • 合併腳本。頁面中的script標籤越少,加載也就越快,響應也更迅速。

  • 無阻塞下載js:

    1. script標籤的defer屬性

    2. 動態建立script元素來下載並執行代碼

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

分享一個還算有趣的前端er:——微信公衆號:劉凱里(kkkyrieliu)
相關文章
相關標籤/搜索