加載第三方 JS 的各種姿勢

dwms5841 8年前發布 | 18K 次閱讀 JavaScript開發 JavaScript

網頁中加載JS文件是一個老問題了,已經被討論了一遍又一遍,這里不會再贅述各種經典的解決方案。JS文件可以通過來源來分為兩個緯度:第一方JS和第三方JS。第一方JS是網頁開發者自己使用的JS代碼(內容開發者可控)。而第三方JS則是其他服務提供商提供的(內容開發者不可控),他們將自己的服務包裝成JS SDK供網頁開發者使用。這篇文章關注的第三方JS文件的加載。

從網站開發者的角度來看,第三方JS相比第一方JS有如下幾個不同之處:

  1. 下載速度不可控

  2. JS地址域名與網站域名不同

  3. 文件內容不可控

  4. 不一定有強緩存(Cache-Control/Expires)

如果你的網站上面有很多第三方JS代碼,那么“下載速度的不可控”很有可能導致你的網站會被拖慢。因為JS在執行的時候會影響到頁面的DOM和樣式等情況。瀏覽器在解析渲染HTML的時候,如果解析到需要下載文件的 script 標簽,那么會停止解析接下來的HTML,然后下載外鏈JS文件并執行。等JS執行完畢之后才會繼續解析剩下的HTML。這就是所謂的『HTML解析被阻止』。瀏覽器解析渲染頁面的抽象流程圖如下:

第三方JS代碼并不受網站開發者的控制,很有可能會出現加載時間長甚至加載失敗的情況。這時候就會導致整個頁面的加載速度變慢。第三方JS代碼越多這種風險越大。按照互聯網守則:

網站加載速度越慢,用戶流失越多

所以要考慮下如何在有很多第三方JS的情況下,保證他們不影響到網站自己的加載速度。我們可以異步加載這些第三方JS代碼。

異步加載

異步加載JS的方法很多,最常見的就是動態創建一個 script 標簽,然后設置其 src 和 async 屬性,再插入到頁面中。這里有個 DEMO 。實際操作的代碼如下:

<script>
function loadScript(url) {
    var scrs = document.getElementsByTagName('script');
    var last = scrs[scrs.length - 1];
    var scr = document.createElement('script');
    scr.src = url;
    scr.async = true;
    last.parentNode.insertBefore(scr, last);
}
loadScript('test.js');
</script>

PS:為了避免 IE8以前版本的bug ,并且確保script能插入DOM樹,所以這里沒有直接 document.body.append(src) ,而是調用了 insertBefore 方法。

改成異步加載第三方JS代碼之后,在JS的下載過程中瀏覽器會繼續解析渲染HTML。流程圖就變成了如下:

因為 loadScript 的操作也是使用JS實現的,所以在JS下載之前會有一段執行JS代碼的消耗。但是這段JS代碼很簡單,很快就會執行完畢。

除了動態創建 script 標簽的方法,異步加載JS的方法還有很多其他奇技淫巧,這里也羅列了一下:

  1. 先下載再執行 - 通過 XMLHttpReqeust 對象或者 JSONP 方法下載可執行的JS文件,然后使用 eval() 或者 script 標簽執行JS。第三方JS文件一般是不同域名的且JS內容不可控,所以此方法就不適用了
  2. iframe 中加載JS – 將你的JS文件直接放到另一個頁面的HTML中,然后將此頁面URL地址作為 iframe 標簽 src 屬性。此方法需要增加一次頁面請求,而且因為是在 iframe 內部執行了,第三方JS文件本身也需要修改,故并不是很適用
  3. 先緩存再執行 – 利用JS文件的強緩存,先使用 new Image().src = 'http://url.com/sample.js' 之類(或者 Object 對象)的 方法 下載JS文件。然后在真正需要解析執行JS的時候下載(有緩存,不必再次下載)和執行JS文件。此方法不僅僅適用于JS文件,同樣也可以用于CSS文件。這樣我們就可以將靜態文件的下載和解析執行(使用)分開,批量并行下載,然后在合適的機會解析執行(使用)。但此方法需要強緩存的配合,第三方JS為了在版本發布時更早的更新JS代碼一般都不會設置緩存,甚至有些第三方JS的代碼是服務器端動態生成的。所以也不是適用于第三方JS。

瀏覽器預加載機制

動態創建 script 標簽的方法可以異步加載第三方JS,但它也有缺陷。如果加載代碼之前有外鏈JS文件或CSS文件需要下載,如下面的代碼:

<script src="app1.js"></script>
<script src="app2.js"></script>
<script>
function loadScript(url) {
    var scrs = document.getElementsByTagName('script');
    var last = scrs[scrs.length - 1];
    var scr = document.createElement('script');
    scr.src = url;
    scr.async = true;
    last.parentNode.insertBefore(scr, last);
}
loadScript('test.js');
</script>

那么會先下載解析 app1.js 和 app2.js 再執行我們的 loadScript 方法,所以第三方腳本的下載也會被暫停。流程圖如下:

而如今我們頁面中代碼如此復雜,觸發這種case的情況很多。上面的 DEMO 中實際下載過程也確實是這樣,動態創建 script 標簽方式下載的test.js需要等到其他CSS和JS文件下載執行完畢之后才開始下載。如下圖:

雖然這對頁面原有JS的執行不會有大的影響,但會影響到第三方JS代碼本身的下載與執行。如何解決這個問題呢?

你可能已經發現上面的例子有個問題:HTML代碼中 g.js 的位置在 test.js 之后卻先下載了。其實這得益于瀏覽器的預解析機制,會先對HTML代碼做靜態分析找到外鏈的JS和CSS文件,然后并行下載下來(但是執行順序不變)。IE>=8 及其他主流瀏覽器基本都實現了這個功能。所以在這些支持預先載的瀏覽器中流程圖應該是這樣的:

為了利用預加載這個特性,我們可以使用如下的寫法:

<script src="app1.js"></script>
<script src="app2.js"></script>
<script src="./test.js" async></script>

使用標準的 script 標簽寫法,確保瀏覽器能夠正確的識別這是一個外鏈JS文件。同時設置 async 標簽,瀏覽器便會異步加載 test.js 文件,不會暫停掉瀏覽器的解析執行。流程圖如下:

但它也并不完美,因為一些 舊瀏覽器 并不支持 async 屬性。這會導致這個 test.js 文件在這些瀏覽器中不是異步的,并且會阻止掉頁面渲染。有一個好消息是移動瀏覽器大多都支持 async 標簽,如果你的用戶大都是移動瀏覽器的,或者你的產品不支持舊瀏覽器,那么你可以使用這種方法。

當然如果你不介意第三方JS代碼(本身也支持支持)被延后到頁面解析完畢后執行,那么你可以再加上 defer 屬性:

<script src="./test.js" async defer></script>

Firefox支持 defer 屬性要比支持 async 早一點點。而且當瀏覽器同時使用了 async 和 defer 屬性之后, 瀏覽器會忽略 defer 屬性 。所以可以放心的同時使用 async 和 defer 屬性。對于不支持 async 的瀏覽器,會自動fallback到 defer 。不過支持程度也就多了一點點,Firefox的舊版占比已經很低了,基本可以忽略不計。

頁面 onload 事件

上面提到的兩種方法還有一個缺點:會影響到頁面的 onload 事件。這對第一方JS可能沒有影響,因為第一方JS大都是頁面主要邏輯,從業務邏輯上來說它們的加載影響到頁面 onload 事件觸發不會有問題。但第三方JS則不一樣,曾經因為Google被墻GA(Google Analytics簡稱)的加載就會特別慢甚至失敗。導致了很多使用了GA的頁面加載特別"慢",頁面一直處于loading狀態。大家先通過fiddler代理來設置 test.js 的加載時間為10秒,然后打開之前的DEMO,查看頁面的loading是否會被延長。下面是我打開第一個 DEMO 的結果:

可以看到因為 test.js 的下載速度變慢,整個頁面一直處于loading狀態。頁面的 load 事件要等到全部加載完成之后才會觸發。如果頁面中的主要邏輯是在頁面 load 之后再執行,那么頁面很可能會在很長一段時間內不可用。極大的影響了用戶的使用體驗。

Friendly IFrame方法

為了解決這個問題,meebo的工程師想了一個方案來解決這個問題:

(function(url){
    // 第一部分
    var dom,doc,where,iframe = document.createElement('iframe');
    iframe.src = "javascript:false";
    iframe.title = ""; iframe.role="presentation";
    (iframe.frameElement || iframe).style.cssText = "width: 0; height: 0; border: 0";
    where = document.getElementsByTagName('script');
    where = where[where.length - 1];
    where.parentNode.insertBefore(iframe, where);

    // 第二部分
    try {
        doc = iframe.contentWindow.document;
    } catch(e) {
        // IE下如果主頁面修改過document.domain,那么訪問用js創建的匿名iframe會發生跨域問題,必須通過js偽協議修改iframe內部的domain
        dom = document.domain;
        iframe.src="javascript:var d=document.open();d.domain='"+dom+"';void(0);";
        doc = iframe.contentWindow.document;
    }
    doc.open()._l = function() {
        var js = this.createElement("script");
        if(dom) this.domain = dom;
        js.id = "js-iframe-async";
        js.src = url;
        this.body.appendChild(js);
    };
    doc.write('<body onload="document._l();">');
    doc.close();
})('test.js');

上述代碼分為兩個部分:

  1. 創建了一個隱藏的 iframe 標簽,設置其 src 值為JS代碼,然后插入到主頁面中

  2. 在 iframe 標簽load之后加載JS腳本

這樣加載Javascript,就不會阻止瀏覽器的 onload 事件,提升普通用戶的體驗。還有另一個好處:第三方的Javascript代碼在獨立的iframe中運行,不會與主頁面中的JS相互干擾。已經有了一些基于這個想法的開源實現,例如:lightning.js是一個專用于快速、安全、異步地加載第三方JS代碼的庫。

這個方法也不完美,它需要創建一個 iframe 標簽導致了開銷較大。同時還需要第三方JS本身的支持。第三方JS代碼運行在iframe中,導致它無法獲取到頁面上的信息。雖然它并非跨域可以獲得 window.parent ,但是第三方代碼并不能知道自己是否在iframe中,需要在加載第三方JS代碼的時候通知它。具體的通知方法千變萬化,而第三方JS的內容又不受我們控制。

富媒體廣告JS(用于展示交互廣告的JS)一般都會運行在隔離環境里面,且不需要(不允許)訪問外部的window對象。如果你需要加載的第三方JS全部是廣告時,那么使用這個方案是OK的,否則并不是最為合適。幸運的是有一個叫 iAB(The Interactive Advertising Bureau,簡稱iAB) 的組織,建立了一套 工業級標準 。雖然標準已經比較舊了,但是里面提到了通過設置變量 inDapIF 為 true 來通知第三方JS:你現在正運行在iframe中。因為iAB成員較多影響力大,所以遵循這個標準是有好處的,比起自己玩一套要好的多。

總結

方法 DEMO 異步 預下載 阻止 onload 事件 比較
動態創建 script 標簽 dynamic_script.html 是(IE<=9除外) 兼容性最好、普適性最高的方案
<script async src="test.js"></script> async_script.html IE>=10及 其他主流瀏覽器 可以 如果你的用戶沒有IE<10(或者偏移動端),那么這是最合適的
<script async defer src="test.js"></script> async_defer.html 如果不介意IE<10中JS的執行會被延后到文檔解析完畢,那么這是最合適的方案
Friendly Iframe friendly_iframe.html 投放代碼太過復雜,且需要第三方JS的支持。比較適用于廣告的加載,因為廣告通常在隔離環境中即可,不需要訪問外部window

 

來自:https://github.com/zmmbreeze/blog/issues/19

 

 本文由用戶 dwms5841 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!