JavaScript阻塞剖析與改善


一、阻塞特性

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

說到無阻塞加載JavaScript要點,我們就有必要知道,為什么在html中不管是內聯JavaScript還是外聯,會影響到頁面的性能?

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

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

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

那什么時候,html解析器才能往下繼續解析html文檔呢?

就JavaScript而言,當html解析器遇到<script>標簽,無論它是內聯還是外聯,頁面中的下載和解析過程都必須停止,直到<script>從外部加載進來的JavaScript或內聯的JavaScript運行完畢,方可繼續解析。在高版本的瀏覽器當中,允許並行下載JavaScript文件,當一個<script>標簽正在下載外部資源時,不必阻塞其他<script>標簽,但是不幸地是,JavaScript的下載仍然會阻塞其他資源的下載,例如圖片。這里還需要值得注意的是,對於樣式和腳本的先后順序同樣會影響到瀏覽器的解析過程,比如將<link>標簽放在<script>標簽前面,如果樣式下載受阻,那么將阻塞<link>后面的<script>加載和執行,究其原因主要在於:script腳本在執行過程中可能會引用到相關樣式。

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

二、改善方法

--最簡單做法--:

為了讓html文檔在解析時,盡量地快,常規的做法是將<script>標簽放到</body>標簽的前面,這樣就不會阻塞html中其他資源的下載了。

如下:

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

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

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

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

  1、  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文件,不會對頁面其余操作影響,原因如下:

  1、html解析器將script當做了dom元素,而不是script標簽,所以就不對其進行諸如加載、解析、運行時,停止頁面中一切行為。打了個擦邊球。

  2、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);
三、拓展閱讀

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

 [2] HTML渲染過程詳解

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

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

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

 [6] 無阻賽JavaScript腳本技術


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



猜您在找 JavaScript——