網頁中載入JS檔案是一個老問題了,已經被討論了一遍又一遍,這裡不會再贅述各種經典的解決方案。JS檔案可以通過來源來分為兩個緯度:第一方JS和第三方JS。第一方JS是網頁開發者自己使用的JS程式碼(內容開發者可控)。而第三方JS則是其他服務提供商提供的(內容開發者不可控),他們將自己的服務包裝成JS SDK供網頁開發者使用。這篇文章關注的第三方JS檔案的載入。
從網站開發者的角度來看,第三方JS相比第一方JS有如下幾個不同之處:
- 下載速度不可控
- JS地址域名與網站域名不同
- 檔案內容不可控
- 不一定有強快取(Cache-Control/Expires)
如果你的網站上面有很多第三方JS程式碼,那麼“下載速度的不可控”很有可能導致你的網站會被拖慢。因為JS在執行的時候會影響到頁面的DOM和樣式等情況。瀏覽器在解析渲染HTML的時候,如果解析到需要下載檔案的script
標籤,那麼會停止解析接下來的HTML,然後下載外鏈JS檔案並執行。等JS執行完畢之後才會繼續解析剩下的HTML。這就是所謂的『HTML解析被阻止』。瀏覽器解析渲染頁面的抽象流程圖如下:
第三方JS程式碼並不受網站開發者的控制,很有可能會出現載入時間長甚至載入失敗的情況。這時候就會導致整個頁面的載入速度變慢。第三方JS程式碼越多這種風險越大。按照網際網路守則:
網站載入速度越慢,使用者流失越多
所以要考慮下如何在有很多第三方JS的情況下,保證他們不影響到網站自己的載入速度。我們可以非同步載入這些第三方JS程式碼。
非同步載入
非同步載入JS的方法很多,最常見的就是動態建立一個script
標籤,然後設定其src
和async
屬性,再插入到頁面中。這裡有個DEMO。實際操作的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 |
<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的方法還有很多其他奇技淫巧,這裡也羅列了一下:
- 先下載再執行 – 通過
XMLHttpReqeust
物件或者JSONP
方法下載可執行的JS檔案,然後使用eval()
或者script
標籤執行JS。第三方JS檔案一般是不同域名的且JS內容不可控,所以此方法就不適用了 iframe
中載入JS – 將你的JS檔案直接放到另一個頁面的HTML中,然後將此頁面URL地址作為iframe
標籤src
屬性。此方法需要增加一次頁面請求,而且因為是在iframe
內部執行了,第三方JS檔案本身也需要修改,故並不是很適用- 先快取再執行 – 利用JS檔案的強快取,先使用
new Image().src = 'http://url.com/sample.js'
之類(或者Object
物件)的方法下載JS檔案。然後在真正需要解析執行JS的時候下載(有快取,不必再次下載)和執行JS檔案。此方法不僅僅適用於JS檔案,同樣也可以用於CSS檔案。這樣我們就可以將靜態檔案的下載和解析執行(使用)分開,批量並行下載,然後在合適的機會解析執行(使用)。但此方法需要強快取的配合,第三方JS為了在版本釋出時更早的更新JS程式碼一般都不會設定快取,甚至有些第三方JS的程式碼是伺服器端動態生成的。所以也不是適用於第三方JS。
瀏覽器預載入機制
動態建立script
標籤的方法可以非同步載入第三方JS,但它也有缺陷。如果載入程式碼之前有外鏈JS檔案或CSS檔案需要下載,如下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<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 及其他主流瀏覽器基本都實現了這個功能。所以在這些支援預先載的瀏覽器中流程圖應該是這樣的:
為了利用預載入這個特性,我們可以使用如下的寫法:
1 2 3 |
<script src="app1.js"></script> <script src="app2.js"></script> <script src="./test.js" async></script> |
使用標準的script
標籤寫法,確保瀏覽器能夠正確的識別這是一個外鏈JS檔案。同時設定async
標籤,瀏覽器便會非同步載入test.js
檔案,不會暫停掉瀏覽器的解析執行。流程圖如下:
這裡有一個DEMO。
但它也並不完美,因為一些舊瀏覽器並不支援async
屬性。這會導致這個test.js
檔案在這些瀏覽器中不是非同步的,並且會阻止掉頁面渲染。有一個好訊息是移動瀏覽器大多都支援async
標籤,如果你的使用者大都是移動瀏覽器的,或者你的產品不支援舊瀏覽器,那麼你可以使用這種方法。
當然如果你不介意第三方JS程式碼(本身也支援支援)被延後到頁面解析完畢後執行,那麼你可以再加上defer
屬性:
1 |
<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的工程師想了一個方案來解決這個問題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
(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'); |
上述程式碼分為兩個部分:
- 建立了一個隱藏的
iframe
標籤,設定其src
值為JS程式碼,然後插入到主頁面中 - 在
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成員較多影響力大,所以遵循這個標準是有好處的,比起自己玩一套要好的多。
總結
|
---|