一個網頁的有很多地方可以進行性能優(yōu)化,比較常見的一種方式就是異步加載js腳本文件蛀蜜。在談異步加載之前办斑,先來看看瀏覽器加載js文件的原理。
瀏覽器加載 JavaScript 腳本赖草,主要通過
<script>
元素完成学少。正常的網頁加載流程是這樣的。
- 瀏覽器一邊下載 HTML 網頁疚顷,一邊開始解析旱易。也就是說,不等到下載完腿堤,就開始解析阀坏。
- 解析過程中,瀏覽器發(fā)現
<script>
元素笆檀,就暫停解析忌堂,把網頁渲染的控制權轉交給 JavaScript 引擎。- 如果
<script>
元素引用了外部腳本酗洒,就下載該腳本再執(zhí)行士修,否則就直接執(zhí)行代碼。- JavaScript 引擎執(zhí)行完畢樱衷,控制權交還渲染引擎棋嘲,恢復解析 HTML 網頁。
加載外部腳本時矩桂,瀏覽器會暫停頁面渲染沸移,等待腳本下載并執(zhí)行完成后,再繼續(xù)渲染侄榴。原因是 JavaScript 代碼可以修改 DOM雹锣,所以必須把控制權讓給它,否則會導致復雜的線程競賽的問題癞蚕。
上面所說的蕊爵,就是我們平時最常見到的,將<script>
標簽放到<head>
中的做法桦山,這樣的加載方式叫做同步加載攒射,或者叫阻塞加載,因為在加載js腳本文件時度苔,會阻塞瀏覽器解析HTML文檔匆篓,等到下載并執(zhí)行完畢之后,才會接著解析HTML文檔寇窑。如果加載時間過長(比如下載時間太長)鸦概,就會造成瀏覽器“假死”,頁面一片空白。而且窗市,放在<head>
中同步加載的js文件中不能對DOM進行操作先慷,否則會產生錯誤,因為這個時候HTML還沒有進行解析咨察,DOM還沒有生成论熙。由此看來,同步加載帶來的體驗往往并不好摄狱。
下面我們來看幾種異步加載的方式脓诡。
-
將
<script>
標簽放到<body>
底部
嚴格來說,這并不算是異步加載媒役,但是這也是常見的通過改變js加載方式來提升頁面性能的一種方式祝谚,所以也就放到這里來說。
將<script>
放到<body>
底部酣衷,解決上上面說到的幾個問題交惯,一是不會造成頁面解析的阻塞,就算加載時間過長用戶也可以看到頁面而不是一片空白穿仪,而且這時候可以在腳本中操作DOM席爽。 -
defer
屬性
通過給<script>
標簽設置defer
屬性,將腳本文件設置為延遲加載啊片,當瀏覽器遇到帶有defer
屬性的<script>
標簽時只锻,會再開啟一個線程去下載js文件,同時繼續(xù)解析HTML文檔紫谷,等等HTML全部解析完畢DOM加載完成之后炬藤,再去執(zhí)行加載好的js文件。
這種方式只適用于引用外部js文件的<script>
標簽碴里,可以保證多個js文件的執(zhí)行順序就是它們在頁面中出現的順序,但是要注意上真,添加defer
屬性的js文件不應該使用document.write方法咬腋。 -
async
屬性
async
屬性和defer
屬性類似,也是會開啟一個線程去下載js文件睡互,但和defer
不同的時根竿,它會在下載完成后立刻執(zhí)行,而不是會等到DOM加載完成之后再執(zhí)行就珠,所以還是有可能會造成阻塞寇壳。
同樣的,async
也是只適用于外部js文件妻怎,也不能在js中使用document.write方法壳炎,但是對多個帶有async
的js文件,它不能像defer那樣保證按順序執(zhí)行逼侦,它是哪個js文件先下載完就先執(zhí)行哪個匿辩。 -
動態(tài)創(chuàng)建
<script>
標簽
可以通過動態(tài)地創(chuàng)建<script>
標簽來實現異步加載js文件腰耙,例如下面代碼:
或者(function(){ var scriptEle = document.createElement("script"); scriptEle.type = "text/javasctipt"; scriptEle.async = true; scriptEle.src = "http://cdn.bootcss.com/jquery/3.0.0-beta1/jquery.min.js"; var x = document.getElementsByTagName("head")[0]; x.insertBefore(scriptEle, x.firstChild); })();
上面兩種方法中,第一種方式執(zhí)行完之前會阻止onload事件的觸發(fā)铲球,而現在很多頁面的代碼都在onload時還執(zhí)行額外的渲染工作挺庞,所以還是會阻塞部分頁面的初始化處理。第二種則不會阻止onload事件的觸發(fā)稼病。(function(){ if(window.attachEvent){ window.attachEvent("load", asyncLoad); }else{ window.addEventListener("load", asyncLoad); } var asyncLoad = function(){ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); } })();
這里要簡要說明一下window.DOMContentLoaded
和window.onload
這兩個事件的區(qū)別选侨,前者是在DOM解析完畢之后觸發(fā),這時候DOM解析完畢然走,JavaScript可以獲取到DOM引用援制,但是頁面中的一些資源比如圖片、視頻等還沒有加載完丰刊,作用同jQuery中的ready事件隘谣。后者則是頁面完全加載完畢,包括各種資源啄巧。
說完了這幾種常見的異步加載js腳本的方式寻歧,再來看最后一個問題,什么時候用defer
秩仆,什么時候用async
呢码泛?一般來說,兩者之間的選擇則是看腳本之間是否有依賴關系澄耍,有依賴的話應當要保證執(zhí)行順序噪珊,應當使用defer
沒有依賴的話使用async
,同時使用的話defer
失效齐莲。要注意的是兩者都不應該使用document.write痢站,這個導致整個頁面被清除。
下面一幅圖表明了同步加載以及defer
选酗、async
加載時的區(qū)別阵难,其中綠色線代表 HTML 解析,藍色線代表網絡讀取js腳本芒填,紅色線代表js腳本執(zhí)行時間: