網(wǎng)頁(yè)中加載JS文件是一個(gè)老問(wèn)題了,已經(jīng)被討論了一遍又一遍,這里不會(huì)再贅述各種經(jīng)典的解決方案。JS文件可以通過(guò)來(lái)源來(lái)分為兩個(gè)緯度:第一方JS和第三方JS。第一方JS是網(wǎng)頁(yè)開(kāi)發(fā)者自己使用的JS代碼(內(nèi)容開(kāi)發(fā)者可控)。而第三方JS則是其他服務(wù)提供商提供的(內(nèi)容開(kāi)發(fā)者不可控),他們將自己的服務(wù)包裝成JS SDK供網(wǎng)頁(yè)開(kāi)發(fā)者使用。這篇文章關(guān)注的第三方JS文件的加載。
從網(wǎng)站開(kāi)發(fā)者的角度來(lái)看,第三方JS相比第一方JS有如下幾個(gè)不同之處:
下載速度不可控
JS地址域名與網(wǎng)站域名不同
文件內(nèi)容不可控
不一定有強(qiáng)緩存(Cache-Control/Expires)
如果你的網(wǎng)站上面有很多第三方JS代碼,那么“下載速度的不可控”很有可能導(dǎo)致你的網(wǎng)站會(huì)被拖慢。因?yàn)镴S在執(zhí)行的時(shí)候會(huì)影響到頁(yè)面的DOM和樣式等情況。瀏覽器在解析渲染HTML的時(shí)候,如果解析到需要下載文件的 script 標(biāo)簽,那么會(huì)停止解析接下來(lái)的HTML,然后下載外鏈JS文件并執(zhí)行。等JS執(zhí)行完畢之后才會(huì)繼續(xù)解析剩下的HTML。這就是所謂的『HTML解析被阻止』。瀏覽器解析渲染頁(yè)面的抽象流程圖如下:

第三方JS代碼并不受網(wǎng)站開(kāi)發(fā)者的控制,很有可能會(huì)出現(xiàn)加載時(shí)間長(zhǎng)甚至加載失敗的情況。這時(shí)候就會(huì)導(dǎo)致整個(gè)頁(yè)面的加載速度變慢。第三方JS代碼越多這種風(fēng)險(xiǎn)越大。按照互聯(lián)網(wǎng)守則:
網(wǎng)站加載速度越慢,用戶流失越多
所以要考慮下如何在有很多第三方JS的情況下,保證他們不影響到網(wǎng)站自己的加載速度。我們可以異步加載這些第三方JS代碼。
異步加載
異步加載JS的方法很多,最常見(jiàn)的就是動(dòng)態(tài)創(chuàng)建一個(gè) script 標(biāo)簽,然后設(shè)置其 src 和 async 屬性,再插入到頁(yè)面中。這里有個(gè) DEMO 。實(shí)際操作的代碼如下:
PS:為了避免 IE8以前版本的bug ,并且確保script能插入DOM樹(shù),所以這里沒(méi)有直接 document.body.append(src) ,而是調(diào)用了 insertBefore 方法。
改成異步加載第三方JS代碼之后,在JS的下載過(guò)程中瀏覽器會(huì)繼續(xù)解析渲染HTML。流程圖就變成了如下:

因?yàn)?loadScript 的操作也是使用JS實(shí)現(xiàn)的,所以在JS下載之前會(huì)有一段執(zhí)行JS代碼的消耗。但是這段JS代碼很簡(jiǎn)單,很快就會(huì)執(zhí)行完畢。
除了動(dòng)態(tài)創(chuàng)建 script 標(biāo)簽的方法,異步加載JS的方法還有很多其他奇技淫巧,這里也羅列了一下:
先下載再執(zhí)行 - 通過(guò) XMLHttpReqeust 對(duì)象或者 JSONP 方法下載可執(zhí)行的JS文件,然后使用 eval() 或者 script 標(biāo)簽執(zhí)行JS。第三方JS文件一般是不同域名的且JS內(nèi)容不可控,所以此方法就不適用了
iframe 中加載JS – 將你的JS文件直接放到另一個(gè)頁(yè)面的HTML中,然后將此頁(yè)面URL地址作為 iframe 標(biāo)簽 src 屬性。此方法需要增加一次頁(yè)面請(qǐng)求,而且因?yàn)槭窃?iframe 內(nèi)部執(zhí)行了,第三方JS文件本身也需要修改,故并不是很適用
先緩存再執(zhí)行 – 利用JS文件的強(qiáng)緩存,先使用 new Image().src = 'http://url.com/sample.js' 之類(或者 Object 對(duì)象)的 方法 下載JS文件。然后在真正需要解析執(zhí)行JS的時(shí)候下載(有緩存,不必再次下載)和執(zhí)行JS文件。此方法不僅僅適用于JS文件,同樣也可以用于CSS文件。這樣我們就可以將靜態(tài)文件的下載和解析執(zhí)行(使用)分開(kāi),批量并行下載,然后在合適的機(jī)會(huì)解析執(zhí)行(使用)。但此方法需要強(qiáng)緩存的配合,第三方JS為了在版本發(fā)布時(shí)更早的更新JS代碼一般都不會(huì)設(shè)置緩存,甚至有些第三方JS的代碼是服務(wù)器端動(dòng)態(tài)生成的。所以也不是適用于第三方JS。
瀏覽器預(yù)加載機(jī)制
動(dòng)態(tài)創(chuàng)建 script 標(biāo)簽的方法可以異步加載第三方JS,但它也有缺陷。如果加載代碼之前有外鏈JS文件或CSS文件需要下載,如下面的代碼:
那么會(huì)先下載解析 app1.js 和 app2.js 再執(zhí)行我們的 loadScript 方法,所以第三方腳本的下載也會(huì)被暫停。流程圖如下:

而如今我們頁(yè)面中代碼如此復(fù)雜,觸發(fā)這種case的情況很多。上面的 DEMO 中實(shí)際下載過(guò)程也確實(shí)是這樣,動(dòng)態(tài)創(chuàng)建 script 標(biāo)簽方式下載的test.js需要等到其他CSS和JS文件下載執(zhí)行完畢之后才開(kāi)始下載。如下圖: