網頁效能優化之非同步載入js檔案

酥風發表於2018-10-22

一個網頁的有很多地方可以進行效能優化,比較常見的一種方式就是非同步載入js指令碼檔案。在談非同步載入之前,先來看看瀏覽器載入js檔案的原理。

瀏覽器載入 JavaScript 指令碼,主要通過<script>元素完成。正常的網頁載入流程是這樣的。

  1. 瀏覽器一邊下載 HTML 網頁,一邊開始解析。也就是說,不等到下載完,就開始解析。
  2. 解析過程中,瀏覽器發現<script>元素,就暫停解析,把網頁渲染的控制權轉交給 JavaScript 引擎。
  3. 如果<script>元素引用了外部指令碼,就下載該指令碼再執行,否則就直接執行程式碼。
  4. JavaScript 引擎執行完畢,控制權交還渲染引擎,恢復解析 HTML 網頁。

載入外部指令碼時,瀏覽器會暫停頁面渲染,等待指令碼下載並執行完成後,再繼續渲染。原因是 JavaScript 程式碼可以修改 DOM,所以必須把控制權讓給它,否則會導致複雜的執行緒競賽的問題。

上面所說的,就是我們平時最常見到的,將<script>標籤放到<head>中的做法,這樣的載入方式叫做同步載入,或者叫阻塞載入,因為在載入js指令碼檔案時,會阻塞瀏覽器解析HTML文件,等到下載並執行完畢之後,才會接著解析HTML文件。如果載入時間過長(比如下載時間太長),就會造成瀏覽器“假死”,頁面一片空白。而且,放在<head>中同步載入的js檔案中不能對DOM進行操作,否則會產生錯誤,因為這個時候HTML還沒有進行解析,DOM還沒有生成。由此看來,同步載入帶來的體驗往往並不好。

下面我們來看幾種非同步載入的方式。

  1. <script>標籤放到<body>底部

    嚴格來說,這並不算是非同步載入,但是這也是常見的通過改變js載入方式來提升頁面效能的一種方式,所以也就放到這裡來說。

    <script>放到<body>底部,解決上上面說到的幾個問題,一是不會造成頁面解析的阻塞,就算載入時間過長使用者也可以看到頁面而不是一片空白,而且這時候可以在指令碼中操作DOM。

  2. defer屬性

    通過給<script>標籤設定defer屬性,將指令碼檔案設定為延遲載入,當瀏覽器遇到帶有defer屬性的<script>標籤時,會再開啟一個執行緒去下載js檔案,同時繼續解析HTML文件,等等HTML全部解析完畢DOM載入完成之後,再去執行載入好的js檔案。

    這種方式只適用於引用外部js檔案的<script>標籤,可以保證多個js檔案的執行順序就是它們在頁面中出現的順序,但是要注意,新增defer屬性的js檔案不應該使用document.write方法。

  3. async屬性

    async屬性和defer屬性類似,也是會開啟一個執行緒去下載js檔案,但和defer不同的時,它會在下載完成後立刻執行,而不是會等到DOM載入完成之後再執行,所以還是有可能會造成阻塞。

    同樣的,async也是隻適用於外部js檔案,也不能在js中使用document.write方法,但是對多個帶有async的js檔案,它不能像defer那樣保證按順序執行,它是哪個js檔案先下載完就先執行哪個。

  4. 動態建立<script>標籤

    可以通過動態地建立<script>標籤來實現非同步載入js檔案,例如下面程式碼:

    (function(){
        var scriptEle = document.createElement("script");
        scriptEle.type = "text/javasctipt";
        scriptEle.async = true;
        scriptEle.src = "http://cdn.bootcss.com/jquery/3.0.0-beta1/jquery.min.js";
        var x = document.getElementsByTagName("head")[0];
        x.insertBefore(scriptEle, x.firstChild); 
    })();
    複製程式碼

    或者

    (function(){
        if(window.attachEvent){
            window.attachEvent("load", asyncLoad);
        }else{
            window.addEventListener("load", asyncLoad);
        }
        var asyncLoad = function(){
        var ga = document.createElement('script'); 
        ga.type = 'text/javascript'; 
        ga.async = true; 
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 
        var s = document.getElementsByTagName('script')[0]; 
        s.parentNode.insertBefore(ga, s);
        }
    })();
    複製程式碼

    上面兩種方法中,第一種方式執行完之前會阻止onload事件的觸發,而現在很多頁面的程式碼都在onload時還執行額外的渲染工作,所以還是會阻塞部分頁面的初始化處理。第二種則不會阻止onload事件的觸發。

    這裡要簡要說明一下window.DOMContentLoadedwindow.onload這兩個事件的區別,前者是在DOM解析完畢之後觸發,這時候DOM解析完畢,JavaScript可以獲取到DOM引用,但是頁面中的一些資源比如圖片、視訊等還沒有載入完,作用同jQuery中的ready事件。後者則是頁面完全載入完畢,包括各種資源。

說完了這幾種常見的非同步載入js指令碼的方式,再來看最後一個問題,什麼時候用defer,什麼時候用async呢?一般來說,兩者之間的選擇則是看指令碼之間是否有依賴關係,有依賴的話應當要保證執行順序,應當使用defer沒有依賴的話使用async,同時使用的話defer失效。要注意的是兩者都不應該使用document.write,這個導致整個頁面被清除。

下面一幅圖表明瞭同步載入以及deferasync載入時的區別,其中綠色線代表 HTML 解析,藍色線代表網路讀取js指令碼,紅色線代表js指令碼執行時間:

同步載入、defer、async的區別

相關文章