一個網頁的有很多地方可以進行效能優化,比較常見的一種方式就是非同步載入js指令碼檔案。在談非同步載入之前,先來看看瀏覽器載入js檔案的原理。
瀏覽器載入 JavaScript 指令碼,主要通過
<script>
元素完成。正常的網頁載入流程是這樣的。
- 瀏覽器一邊下載 HTML 網頁,一邊開始解析。也就是說,不等到下載完,就開始解析。
- 解析過程中,瀏覽器發現
<script>
元素,就暫停解析,把網頁渲染的控制權轉交給 JavaScript 引擎。- 如果
<script>
元素引用了外部指令碼,就下載該指令碼再執行,否則就直接執行程式碼。- JavaScript 引擎執行完畢,控制權交還渲染引擎,恢復解析 HTML 網頁。
載入外部指令碼時,瀏覽器會暫停頁面渲染,等待指令碼下載並執行完成後,再繼續渲染。原因是 JavaScript 程式碼可以修改 DOM,所以必須把控制權讓給它,否則會導致複雜的執行緒競賽的問題。
上面所說的,就是我們平時最常見到的,將<script>
標籤放到<head>
中的做法,這樣的載入方式叫做同步載入,或者叫阻塞載入,因為在載入js指令碼檔案時,會阻塞瀏覽器解析HTML文件,等到下載並執行完畢之後,才會接著解析HTML文件。如果載入時間過長(比如下載時間太長),就會造成瀏覽器“假死”,頁面一片空白。而且,放在<head>
中同步載入的js檔案中不能對DOM進行操作,否則會產生錯誤,因為這個時候HTML還沒有進行解析,DOM還沒有生成。由此看來,同步載入帶來的體驗往往並不好。
下面我們來看幾種非同步載入的方式。
-
將
<script>
標籤放到<body>
底部嚴格來說,這並不算是非同步載入,但是這也是常見的通過改變js載入方式來提升頁面效能的一種方式,所以也就放到這裡來說。
將
<script>
放到<body>
底部,解決上上面說到的幾個問題,一是不會造成頁面解析的阻塞,就算載入時間過長使用者也可以看到頁面而不是一片空白,而且這時候可以在指令碼中操作DOM。 -
defer
屬性通過給
<script>
標籤設定defer
屬性,將指令碼檔案設定為延遲載入,當瀏覽器遇到帶有defer
屬性的<script>
標籤時,會再開啟一個執行緒去下載js檔案,同時繼續解析HTML文件,等等HTML全部解析完畢DOM載入完成之後,再去執行載入好的js檔案。這種方式只適用於引用外部js檔案的
<script>
標籤,可以保證多個js檔案的執行順序就是它們在頁面中出現的順序,但是要注意,新增defer
屬性的js檔案不應該使用document.write方法。 -
async
屬性async
屬性和defer
屬性類似,也是會開啟一個執行緒去下載js檔案,但和defer
不同的時,它會在下載完成後立刻執行,而不是會等到DOM載入完成之後再執行,所以還是有可能會造成阻塞。同樣的,
async
也是隻適用於外部js檔案,也不能在js中使用document.write方法,但是對多個帶有async
的js檔案,它不能像defer那樣保證按順序執行,它是哪個js檔案先下載完就先執行哪個。 -
動態建立
<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指令碼執行時間: