一個網頁的有很多地方可以進行性能優化,比較常見的一種方式就是異步加載js腳本文件。在談異步加載之前,先來看看瀏覽器加載js文件的原理。
瀏覽器加載 JavaScript 腳本,主要通過
<script>元素完成。正常的網頁加載流程是這樣的。
- 瀏覽器一邊下載 HTML 網頁,一邊開始解析。也就是說,不等到下載完,就開始解析。
- 解析過程中,瀏覽器發現
<script>元素,就暫停解析,把網頁渲染的控制權轉交給 JavaScript 引擎。- 如果
<script>元素引用了外部腳本,就下載該腳本再執行,否則就直接執行代碼。- JavaScript 引擎執行完畢,控制權交還渲染引擎,恢復解析 HTML 網頁。
加載外部腳本時,瀏覽器會暫停頁面渲染,等待腳本下載並執行完成后,再繼續渲染。原因是 JavaScript 代碼可以修改 DOM,所以必須把控制權讓給它,否則會導致復雜的線程競賽的問題。
上面所說的,就是我們平時最常見到的,將`<script>`標簽放到`<head>`中的做法,這樣的加載方式叫做同步加載,或者叫阻塞加載,因為在加載js腳本文件時,會阻塞瀏覽器解析HTML文檔,等到下載並執行完畢之后,才會接着解析HTML文檔。如果加載時間過長(比如下載時間太長),就會造成瀏覽器“假死”,頁面一片空白。而且,放在`<head>`中同步加載的js文件中不能對DOM進行操作,否則會產生錯誤,因為這個時候HTML還沒有進行解析,DOM還沒有生成。由此看來,同步加載帶來的體驗往往並不好。
下面我們來看幾種異步加載的方式。
1. 將<script>標簽放到<body>底部
嚴格來說,這並不算是異步加載,但是這也是常見的通過改變js加載方式來提升頁面性能的一種方式,所以也就放到這里來說。
將<script>放到<body>底部,解決上上面說到的幾個問題,一是不會造成頁面解析的阻塞,就算加載時間過長用戶也可以看到頁面而不是一片空白,而且這時候可以在腳本中操作DOM。
2. defer屬性
通過給<script>標簽設置defer屬性,將腳本文件設置為延遲加載,當瀏覽器遇到帶有defer屬性的<script>標簽時,會再開啟一個線程去下載js文件,同時繼續解析HTML文檔,等等HTML全部解析完畢DOM加載完成之后,再去執行加載好的js文件。
這種方式只適用於引用外部js文件的<script>標簽,可以保證多個js文件的執行順序就是它們在頁面中出現的順序,但是要注意,添加defer屬性的js文件不應該使用document.write方法。
3. async屬性
async屬性和defer屬性類似,也是會開啟一個線程去下載js文件,但和defer不同的時,它會在下載完成后立刻執行,而不是會等到DOM加載完成之后再執行,所以還是有可能會造成阻塞。
同樣的,async也是只適用於外部js文件,也不能在js中使用document.write方法,但是對多個帶有async的js文件,它不能像defer那樣保證按順序執行,它是哪個js文件先下載完就先執行哪個。
4. 動態創建<script>標簽
可以通過動態地創建<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);
})();
或者
(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);
}
})();
上面兩種方法中,第一種方式執行完之前會阻止onload事件的觸發,而現在很多頁面的代碼都在onload時還執行額外的渲染工作,所以還是會阻塞部分頁面的初始化處理。第二種則不會阻止onload事件的觸發。
這里要簡要說明一下window.DOMContentLoaded和window.onload這兩個事件的區別,前者是在DOM解析完畢之后觸發,這時候DOM解析完畢,JavaScript可以獲取到DOM引用,但是頁面中的一些資源比如圖片、視頻等還沒有加載完,作用同jQuery中的ready事件。后者則是頁面完全加載完畢,包括各種資源。
說完了這幾種常見的異步加載js腳本的方式,再來看最后一個問題,什么時候用defer,什么時候用async呢?一般來說,兩者之間的選擇則是看腳本之間是否有依賴關系,有依賴的話應當要保證執行順序,應當使用defer沒有依賴的話使用async,同時使用的話defer失效。要注意的是兩者都不應該使用document.write,這個導致整個頁面被清除。
下面一幅圖表明了同步加載以及defer、async加載時的區別,其中綠色線代表 HTML 解析,藍色線代表網絡讀取js腳本,紅色線代表js腳本執行時間:

