異步加載JS腳本

JavaScript腳本對(duì)現(xiàn)代網(wǎng)站來(lái)說(shuō)是必不可少的。當(dāng)用戶訪問(wèn)站點(diǎn),需要下載各種資源,例如JS腳本,CSS,圖片,iframe等。

瀏覽器下載除JS外的資源時(shí),會(huì)并行下載,以提高性能。但下載JS腳本時(shí),會(huì)禁止并行下載(稱為腳本阻塞Scripts Block Downloads)。瀏覽器遇到JS時(shí),必須等JS下載,解析,執(zhí)行完后,才能繼續(xù)并行下載下一個(gè)資源。原因是JS可能會(huì)改變頁(yè)面或改變JS間的依賴關(guān)系,例如A.js中用document.write改變頁(yè)面,B.js依賴于A.js。因此要嚴(yán)格保證順序,不能并行下載。

(你可以點(diǎn)擊這里,試一下本篇中所有例子)

因此不推薦將JS放到<head>標(biāo)簽里,你可以點(diǎn)擊例子頁(yè)面試一下。瀏覽器遇到<script>標(biāo)簽會(huì)下載,解析,執(zhí)行完腳本后,才繼續(xù)處理剩余的頁(yè)面部分。而且瀏覽器在遇到<body>標(biāo)簽前是不會(huì)渲染頁(yè)面的,因此例子中處理JS用了2s,頁(yè)面會(huì)白屏2s。

為了避免白屏,通常的建議是將JS放到<body>標(biāo)簽底下,可以有最佳的用戶體驗(yàn),你可以點(diǎn)擊例子頁(yè)面試一下。如果你放到了<body>標(biāo)簽的上部,因?yàn)槟_本阻塞,可能會(huì)影響用戶體驗(yàn)。例子中位于<body>上部的JS耗時(shí)2s,因此位于JS下面的兩張img圖片,會(huì)等2s后才開(kāi)始加載。

將JS放在<body>底下的建議沒(méi)有錯(cuò),幾乎成了前端的普世規(guī)則。但對(duì)于追求極致用戶體驗(yàn)的站點(diǎn),或大型網(wǎng)站來(lái)說(shuō),這還不夠。雖然JS腳本由于存在的依賴關(guān)系,需要按順序執(zhí)行,但并不需要按順序下載。本篇就介紹一下讓JS與其他組件并行下載的方法,即異步處理腳本。

  • 異步處理外部腳本:(按推薦度從高到低排列)
  • Dynamic Script Element
  • Script async
  • Script defer
  • XHR Eval
  • XHR Inject
  • Script in iframe
  • 直接寫入HTML
  • document.write Script
  • 忙指示器
  • 異步處理外部腳本總結(jié)
  • 異步處理行內(nèi)腳本

Dynamic Script Element

通常我們加載JS腳本會(huì)在HTML里:

<script type="text/javascript" src="A.js"></script>

這屬于靜態(tài)腳本元素,瀏覽器執(zhí)行到這里發(fā)現(xiàn)script元素,會(huì)按上面所說(shuō)的,下載解析執(zhí)行腳本,同時(shí)阻塞其他資源文件的下載。而動(dòng)態(tài)腳本元素如下:

var script = document.createElement('script');  //創(chuàng)建script標(biāo)簽
script.type = "text/javascript";
script.src = "A.js";
document.getElementsByTagName('head')[0].appendChild(script);   //塞進(jìn)頁(yè)面

先用document.createElement(‘script’)生成一個(gè)script標(biāo)簽,再設(shè)置它的src屬性,最后將其插入到<head>中。

script標(biāo)簽被插入到頁(yè)面的DOM樹(shù)后,就會(huì)開(kāi)始下載src屬性指定的腳本。而且通過(guò)動(dòng)態(tài)腳本元素下載腳本是異步的,不會(huì)阻塞頁(yè)面的其他下載和處理過(guò)程,因此script標(biāo)簽插入<head>中也沒(méi)問(wèn)題。

當(dāng)JS下載完畢后,就會(huì)立即執(zhí)行。如果多個(gè)JS間有依賴關(guān)系,一下載完馬上執(zhí)行可能會(huì)出現(xiàn)error。因此通常來(lái)說(shuō)你應(yīng)該將有依賴關(guān)系的JS合并成一個(gè)文件,雖然合并后JS文件會(huì)變大,但由于是異步下載,你幾乎不會(huì)有什么損失。

如果實(shí)在不方便將有依賴關(guān)系的文件合并。你需要自己指定先后順序,通過(guò)監(jiān)聽(tīng)load事件(IE是onreadystatechange)來(lái)確保依次加載腳本:

function loadScript(url, callback){
    var script = document.createElement ("script")
    script.type = "text/javascript";

    if (script.readyState){ //IE
        script.onreadystatechange = function(){
            if (script.readyState == "loaded" || script.readyState == "complete"){
                script.onreadystatechange = null;
                callback();
            }
        };
    } else { //Others
        script.onload = function(){
            callback();
        };
    }
    script.src = url;
    document.getElementsByTagName("head")[0].appendChild(script);
}

//嚴(yán)格確保A->B->C,依次下載腳本文件
loadScript("A-delay.js", function(){
    loadScript("B-delay.js", function(){
        loadScript("C-delay.js", function(){
            console.log("All files are loaded!");
        });
    });
});

該技術(shù)不但簡(jiǎn)單,而且通用,且可以跨域,應(yīng)該成為你的首選。

Script async

HTML5里為script標(biāo)簽里新增了async屬性,用于異步加載腳本:

<script type="text/javascript" src="alert.js" async="async"></script>

瀏覽器解析到HTML里的該行script標(biāo)簽,發(fā)現(xiàn)指定為async,會(huì)異步下載解析執(zhí)行腳本。

例子頁(yè)面的DOM結(jié)構(gòu)里由于script在img圖片之前,如果你的瀏覽器支持async的話,就會(huì)異步加載腳本。此時(shí)DOM里已經(jīng)有img元素了,所以腳本里能順利取到img的src并彈框。

該方式可以跨域。缺點(diǎn)是HTML5里新增屬性,如果你需要繼續(xù)支持舊版本瀏覽器可能需要打些補(bǔ)丁。

Script defer

script標(biāo)簽里可以設(shè)置defer,表示延遲加載腳本:

<script type="text/javascript" src="alert.js" defer="defer"></script>

瀏覽器解析到HTML里的該行script標(biāo)簽,發(fā)現(xiàn)指定為defer,會(huì)暫緩下載解析執(zhí)行腳本。而是等到頁(yè)面加載完畢后,才加載腳本(更精確地說(shuō),是在DOM樹(shù)構(gòu)建完成后,在window.onload觸發(fā)前,加載defer的腳本)。

例子頁(yè)面的DOM結(jié)構(gòu)里由于script在img圖片之前,如果你的瀏覽器支持defer的話,就會(huì)延遲到頁(yè)面加載完后才下載腳本。此時(shí)DOM里已經(jīng)有img元素了,所以腳本里能順利取到img的src并彈框。

該方式可以跨域。缺點(diǎn)是雖然不像async屬于HTML5的新屬性,但defer仍舊有舊版本瀏覽器不支持。(根據(jù)w3cschool的說(shuō)法,只有IE瀏覽器支持defer,試驗(yàn)下來(lái)其他瀏覽器新版均已支持defer,所以w3cschool的說(shuō)法已經(jīng)過(guò)時(shí))

XHR Eval

通過(guò)傳統(tǒng)的Ajax方式,用XMLHttpRequest(低版本IE中是ActiveXObject)異步獲得腳本內(nèi)容后,通過(guò)eval執(zhí)行腳本:

function createRequest() {  
    try {  
        request = new XMLHttpRequest();  
    } catch (tryMS) {  
        try {             
            request = new ActiveXObject("Msxml2.XMLHTTP");    
        } catch (otherMS) {   
            try {  
                request = new ActiveXObject("Microsoft.XMLHTTP");  
            } catch (failed) {  
                request = null;  
            }  
        }  
    }
    return request;  
}  

var request = createRequest();    //獲得一個(gè)請(qǐng)求對(duì)象
request.onreadystatechange = function() {
    if (request.readyState == 4) {
        if ((request.status >= 200 && request.status < 300) || request.status == 304) {
            eval(request.responseText);
        }
    }
};
request.open("GET", "alert.js", true);
request.send(null);

這種Ajax方式能讓瀏覽器異步加載腳本,不會(huì)阻塞其他資源的下載。缺點(diǎn)是不支持跨域,因此腳本和頁(yè)面必須在同一個(gè)域。

例子頁(yè)面的DOM結(jié)構(gòu)里由于腳本在img圖片之前,如果不是異步加載腳本的話,是無(wú)法alert出圖片的URL地址的。

另外應(yīng)該會(huì)有人表示eval有性能上的問(wèn)題,理論上確實(shí)如此,但還是得以實(shí)測(cè)為準(zhǔn),可能需要你在幾ms的性能問(wèn)題和異步下載帶來(lái)的優(yōu)勢(shì)之間做出權(quán)衡。還有人表示eval有安全隱患,理論上確實(shí)如此。但因?yàn)椴荒芸缬?,通常站點(diǎn)里的腳本你應(yīng)該有絕對(duì)的控制權(quán)。如果JS里不涉及input元素,我不認(rèn)為會(huì)出現(xiàn)什么安全隱患…當(dāng)然如果你對(duì)eval深惡痛絕,也可以換成XHR Inject方式。

XHR Inject

和XHR Eval的區(qū)別是:用傳統(tǒng)Ajax方式下載腳本后,不再通過(guò)eval,而是動(dòng)態(tài)創(chuàng)建script元素后塞進(jìn)頁(yè)面。將XHR Eval里的eval部分替換成下面代碼即可:

var script = document.createElement("script");
script.type = "text/javascript";
script.text = request.responseText;
document.body.appendChild(script);

動(dòng)態(tài)創(chuàng)建的script元素將被塞到body底部,塞進(jìn)去之后就會(huì)開(kāi)始執(zhí)行腳本。優(yōu)缺點(diǎn)基本同XHR Eval,同樣不能跨越。雖然避免了eval性能和安全的問(wèn)題,但效率可能比XHR Eval稍微低一點(diǎn)。

Script in iframe

利用iframe可以和其他組件并行下載的特性,將腳本嵌入到iframe里實(shí)現(xiàn)異步加載。缺點(diǎn)同樣是不能跨越,腳本和頁(yè)面必須在同一個(gè)域中。

<iframe src='alert.html' width=0 height=0 frameborder=0 id='iframe1'></iframe>

創(chuàng)建iframe,注意src里是alert.html,而非alert.js。需要新建一個(gè)alert.html,將alert.js內(nèi)容內(nèi)嵌進(jìn)去。

該技術(shù)的缺點(diǎn)除了不能跨域外,還包括iframe的共通問(wèn)題。例如iframe的開(kāi)銷比其他DOM元素高很多。iframe會(huì)阻塞onload事件,而我們通常希望onload能盡快觸發(fā),因?yàn)閛nload觸發(fā)時(shí)瀏覽器會(huì)停止忙指示器,能讓用戶感覺(jué)快了一點(diǎn)。

直接寫入HTML

簡(jiǎn)單粗暴無(wú)內(nèi)涵,將外部腳本的內(nèi)容拷貝粘貼進(jìn)HTML:

<script type="text/javascript">
    function(...);
    ...
</script>

這個(gè)方法的優(yōu)點(diǎn)是可以嚴(yán)格保證JS順序,還能減少HTTP請(qǐng)求。缺點(diǎn)是不能享受不到瀏覽器緩存。由于緩存對(duì)站點(diǎn)來(lái)說(shuō)如此的重要,因此不推薦這個(gè)方法。況且該方法會(huì)增大HTML體積,因此只有極少數(shù)情況下,例如網(wǎng)站首頁(yè)非常重要的內(nèi)容,且JS代碼較少時(shí),才會(huì)考慮將JS直接寫入HTML里。

document.write Script

在IE中可以用document.write把script元素寫入HTML可以實(shí)現(xiàn)并行下載腳本,但下載腳本時(shí)仍舊會(huì)對(duì)其他資源造成腳本阻塞:

document.write("<script type=\"text/javascript\" src=\"A-delay.js\"><\/script>");
document.write("<script type=\"text/javascript\" src=\"B-delay.js\"><\/script>");

該特性適用范圍很窄,我只在IE9上確認(rèn)過(guò)?;静煌扑]。

忙指示器

瀏覽器在加載時(shí)有4種方式提示用戶:標(biāo)簽頁(yè)前的菊花轉(zhuǎn)(見(jiàn)下圖),左下角顯示”已連接到 xxxx”(見(jiàn)下圖),底部中央顯示進(jìn)度條,鼠標(biāo)成漏斗狀。后兩者因?yàn)樘螅诂F(xiàn)代瀏覽器中已被隱藏,老式瀏覽器比較明顯。

上面介紹的異步方式中,通過(guò)script技術(shù)下載腳本會(huì)觸發(fā)瀏覽器這些忙指示器。但XHR方式不會(huì)觸發(fā)瀏覽器的忙指示器。你需要權(quán)衡哪些需要讓用戶知道正在下載,哪些不需要讓用戶知道。

異步處理外部腳本總結(jié)

對(duì)上面介紹的方法做一下總結(jié)。具有共通性的跨域,和忙指示器見(jiàn)下圖:


通常來(lái)說(shuō)你應(yīng)該放棄document.write Script方式。只有在很少情況下才會(huì)選用直接寫入HTML的方式。

如果資源需要跨域,你需要放棄XHR Eval,XHR Inject,Script in iframe方式

如果需要兼容舊式瀏覽器,你可能需要放棄Script async,Script defer

常規(guī)的<script type=”text/javascript” src=”xxx.js”></script>方式加載外部腳本,因?yàn)橛心_本阻塞問(wèn)題,可能會(huì)出現(xiàn)白屏,頁(yè)面卡等問(wèn)題。程度視加載腳本耗時(shí)而定。用本篇介紹的方法來(lái)加載外部腳本可以大大改善用戶體驗(yàn)。

異步處理行內(nèi)腳本

直接寫進(jìn)HTML的JS代碼雖然不存在下載的問(wèn)題,但需要考慮執(zhí)行速度。如果執(zhí)行速度慢,同樣會(huì)有腳本阻塞的問(wèn)題。但相比外部腳本,處理行內(nèi)腳本就簡(jiǎn)單多了。常見(jiàn)的方式如下:(按推薦度從高到低排列)

行內(nèi)腳本移到HTML底部:不贅述。

異步執(zhí)行行內(nèi)腳本:耗時(shí)小用setTimeout,等價(jià)于移到了HTML底部。耗時(shí)多用window.onload。例如:

<script type="text/javascript">
    function doSomething() {…}
    setTimeout(doSomething, 0);
</script>

如果行內(nèi)代碼執(zhí)行的速度很快,像上面那樣將setTimeout的時(shí)間設(shè)成0就行了。如果執(zhí)行很耗時(shí),為了逐步渲染,你可以用綁定到window.onload事件上:

<script type="text/javascript">
    function doSomething() {…}
    window.onload = doSomething;
</script>

Script defer:defer屬性同樣適用于行內(nèi)腳本,具體參照上面,不贅述。

總結(jié)

看到這里你可能會(huì)很疑惑,現(xiàn)在大型前端開(kāi)發(fā)中加載JS,會(huì)用一些現(xiàn)成的加載工具,如AMD的requirejs,seajs, mass,oyejs等,它們既能異步下載,又能保證順序,用起來(lái)又方便。知道這些有什么用呢?這就看得你的追求了,好比用jQuery能讓新手很快速上手進(jìn)行開(kāi)發(fā),但JavaScript基礎(chǔ)不好,估計(jì)也只能用jQuery做點(diǎn)簡(jiǎn)單開(kāi)發(fā)。理解了本篇,對(duì)你查看seajs等源碼有好處,能讓你對(duì)這些工具有更感性的認(rèn)識(shí)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 異步處理外部腳本總結(jié) Dynamic Script Element 通常我們加載JS腳本...
    前端小華子閱讀 566評(píng)論 0 0
  • 本文總結(jié)一下瀏覽器在 javascript 的加載方式。關(guān)鍵詞:異步加載(async loading),延遲加載(...
    4ea0af17fd67閱讀 1,118評(píng)論 0 2
  • 首先我們先來(lái)看一下Script標(biāo)簽的各項(xiàng)屬性: script標(biāo)簽也支持HTML中的全局屬性: 下面我們來(lái)看看一看j...
    tobAlier閱讀 1,282評(píng)論 0 2
  • 前端開(kāi)發(fā)面試知識(shí)點(diǎn)大綱: HTML&CSS: 對(duì)Web標(biāo)準(zhǔn)的理解、瀏覽器內(nèi)核差異、兼容性、hack、CSS基本功:...
    秀才JaneBook閱讀 2,792評(píng)論 0 25
  • 那時(shí)候,我們還在山野奔跑,那時(shí)候,我們還沒(méi)有電腦和手機(jī),那時(shí)候,我還有我的青梅和竹馬。我的青梅竹馬是對(duì)姐弟,青梅比...
    偶爾想起來(lái)的小號(hào)閱讀 190評(píng)論 0 0

友情鏈接更多精彩內(nèi)容