JavaScript阻塞剖析與改善

1、阻塞特性

《高性能JavaScript》一書中,關於第一章「Loading and Execution」,提到了無阻塞加載JavaScript技術,目的是爲了提升頁面呈現速度。javascript

說到無阻塞加載JavaScript要點,咱們就有必要知道,爲何在html中不論是內聯JavaScript仍是外聯,會影響到頁面的性能?css

緣由是:JavaScript是單線程,在JavaScript運行時其餘的事情不能被瀏覽器處理。事實上,大多數瀏覽器使用單線程處理UI更新和JavaScript運行等多個任務,而同一時間只能有一個任務被執行。因此在執行JavaScript時,會妨礙其餘頁面動做。這是JavaScript的特性,咱們無法改變。html

而且,html解析過程是至上而下的,當html解析器遇到諸如<script>、<link>等標籤時,解析器就會中止下來,去下載相應的內容。須要注意的是,在加載<script>、<link>標籤時都會阻止解析器往下執行。html5

而且,html解析過程是至上而下的,當html解析器遇到諸如<script>、<link>等標籤時,就會去下載相應內容。且加載、解析、執行JavaScript會阻止解析器往下執行。java

那何時,html解析器才能往下繼續解析html文檔呢?跨域

就JavaScript而言,當html解析器遇到<script>標籤,不管它是內聯仍是外聯,頁面中的下載和解析過程都必須中止,直到<script>從外部加載進來的JavaScript或內聯的JavaScript運行完畢,方可繼續解析。在高版本的瀏覽器當中,容許並行下載JavaScript文件,當一個<script>標籤正在下載外部資源時,沒必要阻塞其餘<script>標籤,可是不幸地是,JavaScript的下載仍然會阻塞其餘資源的下載,例如圖片。這裏還須要值得注意的是,對於樣式和腳本的前後順序一樣會影響到瀏覽器的解析過程,好比將<link>標籤放在<script>標籤前面,若是樣式下載受阻,那麼將阻塞<link>後面的<script>加載和執行,究其緣由主要在於:script腳本在執行過程當中可能會引用到相關樣式。瀏覽器

瞭解了JavaScript在html中的阻塞特性,咱們再來看看如何改善其阻塞特性。安全

2、改善方法

--最簡單作法--:app

爲了讓html文檔在解析時,儘可能地快,常規的作法是將<script>標籤放到</body>標籤的前面,這樣就不會阻塞html中其餘資源的下載了。dom

以下:

儘管腳本下載之間互相阻塞,但頁面已經下載完成而且顯示在用戶面前了,進入頁面的速度不會顯得太慢。且,爲了讓腳本之間的互相阻塞最小化,一般將多個相關的JavaScript文件合併爲一個JavaScript文件,另外這樣作帶來的好處不只讓腳本之間阻塞變小,還減小了http請求的數量。

但,這樣作JavaScript文件下載之間仍是會阻塞,特別是當JavaScript文件逐漸變多時。

故而,引入無阻塞腳本技術。

無阻塞腳本技術主要分爲兩大類:

  一、  HTML5中的defer和async;

  2  動態建立script爲dom元素。

下面將分別介紹。

--HTML5中的defer和async--:

HTML5中提供了兩個屬性供<script>標籤使用,目的就是爲了無阻塞加載JavaScript。

用法以下:

<script src="file1.js" defer></script>
<script src="file2.js" async></script>

須要注意的是,這兩個屬性對內聯JavaScript是無效的,只針對外聯JavaScript,如上所示。

加載流程:

當解析器遇到設置defer或者async屬性的<script>元素時,它開始下載腳本,並繼續解析文檔。腳本會在它下載完成後儘快執行,可是解析器沒有停下來等待他下載。

defer和async區別:

就defer和async的區別而言,使用defer的<script>標籤是按照他們排列的順序執行的,而使用async的<script>標籤是不按他們在HTML中的排列順序執行的;

就執行時間而言,defer是在DOMContentloaded事件以前執行,而async是在window.onload事件以前執行的,且只支持IE10+。當defer和async同時存在時,會忽略defer而遵循async。且使用defer和async的腳本禁止使用document.write方法哦。

--動態腳本元素--:

由於script標籤是在html中的,是屬於dom元素,因此咱們徹底能夠利用dom方法建立一個動態的script元素。

以下:

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

當建立的script元素添加到頁面後馬上開始下載。此技術的重點在於:不管在何處啓動下載,文件的下載和運行都不會阻塞其餘頁面的處理過程。你甚至能夠將這些代碼放在<head>部分而不會對其他部分的頁面代碼形成影響(除了用於下載文件的HTTP鏈接)

上面加粗部分引至《高性能JavaScript》,當時在我讀到這句話時,不是很理解,在前面「阻塞特性」一小節中,咱們提到JavaScript是單線程且與UI線程互排,那麼JavaScript在運行時,怎麼不會阻塞其餘頁面的處理過程呢?

爲此,帶着這一困惑在博客園問答中心提出了本身的觀點並與道友討論(‘博問點擊此’)

經過與道友討論以及本身查看了相關文檔後,有了本身看法:

之因此動態建立script元素去加載JavaScript文件,不會對頁面其他操做影響,緣由以下:

  一、html解析器將script當作了dom元素,而不是script標籤,因此就不對其進行諸如加載、解析、運行時,中止頁面中一切行爲。打了個擦邊球。

  二、JavaScript是單線程,且與UI線程共享同一個線程,但這不表明瀏覽器就只有一個線程。因此在執行JavaScript代碼時,不影響圖片之類的下載。

好了,回到剛纔採用動態腳本元素的方法,咱們還得完善下,緣由是上述代碼,在‘自運行’時還好,可是若是引用了其餘js文件中的方法呢?那就得出錯咯。由於咱們沒法保證動態腳本元素執行JavaScript代碼的順序。針對這一問題,標準瀏覽器咱們能夠利用<script>節點的load事件處理,而IE瀏覽器咱們能夠利用其特有的readystatechange事件處理。

封裝好的代碼以下:

function loadScript(url, callback){
    var script = document.createElement('script');
    script.type = 'text/javascript';
    /*
        在IE中readyState值所表示的最終狀態並不一致,
        有時<script>元素會獲得"loaded"卻不出現"complete",
        但另一些狀況下出現"complete"而用不到"loaded"。
        最安全的辦法就是在readystatechange事件中檢查這兩種狀態,
        而且當其中一種狀態出現時,刪除readystatechange事件句柄(保證事件不會被觸發兩次)
    */
    if(script.readyState){//IE
        script.onreadystatechange = function(){
            if(script.readyState == 'loaded' || script.readyState == 'complete'){
                script.onreadystatechange = null;
                callback()
            }
        }
    }else{//Other
        script.onload = function(){
            callback();    
        }
    }
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script);
}

因此,當頁面中動態加載多個有關聯的JavaScript文件時,咱們能夠將其串聯起來,保證順序。

以下:

//串聯起來
loadScript('file1.js',function(){
    loadScript('file2.js',function(){
        ...
    });
});

除開這種方法,還有一種就是「XHR腳本注入」,大致內容與上面的方法差很少,都須要動態建立script元素,區別在於該方法利用XMLHttpRequest對象,請求JavaScript文件,並將請求到的responseText,插入script元素的text中。由於是藉助XMLHttpRequest對象,缺點顯而易見,不能跨域請求。

示例代碼以下:

var xhr = new XMLHttpRequest();
xhr.open('get', 'file1.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);
3、拓展閱讀

 [1] JavaScript是單線程的深刻分析

 [2] HTML渲染過程詳解

 [3] 瀏覽器加載渲染網頁過程解析

 [4] defer、async屬性以及JS異步加載並執行解決方案

 [5] HTML5 <script>元素async,defer異步加載

 [6] 無阻賽JavaScript腳本技術

相關文章
相關標籤/搜索