載入第三方JS的各種姿勢

發表於2017-07-02

網頁中載入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標籤,然後設定其srcasync屬性,再插入到頁面中。這裡有個DEMO。實際操作的程式碼如下:

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檔案需要下載,如下面的程式碼:

那麼會先下載解析app1.jsapp2.js再執行我們的loadScript方法,所以第三方指令碼的下載也會被暫停。流程圖如下:

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

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

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

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

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

這裡有一個DEMO

但它也並不完美,因為一些舊瀏覽器並不支援async屬性。這會導致這個test.js檔案在這些瀏覽器中不是非同步的,並且會阻止掉頁面渲染。有一個好訊息是移動瀏覽器大多都支援async標籤,如果你的使用者大都是移動瀏覽器的,或者你的產品不支援舊瀏覽器,那麼你可以使用這種方法。

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

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

頁面onload事件

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

loading

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

Friendly IFrame方法

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

上述程式碼分為兩個部分:

  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)的組織,建立了一套工業級標準。雖然標準已經比較舊了,但是裡面提到了通過設定變數inDapIFtrue來通知第三方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

相關文章