在現代網站中,腳本通常「比」HTML「重」:它們的下載大小較大,處理時間也較長。
當瀏覽器載入 HTML 並遇到 <script>...</script>
標籤時,它無法繼續建立 DOM。它必須立即執行腳本。外部腳本 <script src="..."></script>
也是如此:瀏覽器必須等到腳本下載、執行已下載的腳本,然後才能處理頁面的其餘部分。
這會導致兩個重要的問題
- 腳本無法看到它們下方的 DOM 元素,因此它們無法新增處理常式等。
- 如果頁面頂端有一個龐大的腳本,它會「封鎖頁面」。使用者無法在腳本下載並執行之前看到頁面內容
<p>...content before script...</p>
<script src="https://javascriptinfo.dev.org.tw/article/script-async-defer/long.js?speed=1"></script>
<!-- This isn't visible until the script loads -->
<p>...content after script...</p>
有一些解決方法。例如,我們可以將腳本放在頁面底部。然後它可以看到它上方的元素,並且不會封鎖頁面內容的顯示
<body>
...all content is above the script...
<script src="https://javascriptinfo.dev.org.tw/article/script-async-defer/long.js?speed=1"></script>
</body>
但這個解決方案遠非完美。例如,瀏覽器只有在下載完完整的 HTML 文件後才會注意到腳本(並開始下載腳本)。對於較長的 HTML 文件,這可能會造成明顯的延遲。
對於使用非常快速連線的人來說,這些事情是看不見的,但世界上許多人仍然有慢速的網際網路速度,並使用遠非完美的行動網路連線。
幸運的是,有兩個 <script>
屬性可以幫我們解決這個問題:defer
和 async
。
defer
defer
屬性告訴瀏覽器不要等待腳本。瀏覽器將繼續處理 HTML,建立 DOM。腳本在「背景中」載入,然後在 DOM 完全建立後執行。
以下是與上述相同的範例,但使用了 defer
<p>...content before script...</p>
<script defer src="https://javascriptinfo.dev.org.tw/article/script-async-defer/long.js?speed=1"></script>
<!-- visible immediately -->
<p>...content after script...</p>
換句話說
- 具有
defer
的腳本絕不會阻擋頁面。 - 具有
defer
的腳本總是在 DOM 準備就緒時執行(但在DOMContentLoaded
事件之前)。
以下範例示範第二個部分
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>
<script defer src="https://javascriptinfo.dev.org.tw/article/script-async-defer/long.js?speed=1"></script>
<p>...content after scripts...</p>
- 頁面內容立即顯示。
DOMContentLoaded
事件處理常式等待遞延腳本。它只會在腳本下載並執行時觸發。
遞延腳本會保留其相對順序,就像一般腳本一樣。
假設我們有兩個遞延腳本:long.js
,然後是 small.js
<script defer src="https://javascriptinfo.dev.org.tw/article/script-async-defer/long.js"></script>
<script defer src="https://javascriptinfo.dev.org.tw/article/script-async-defer/small.js"></script>
瀏覽器會掃描頁面中的腳本並平行下載,以提升效能。因此,在上述範例中,兩個腳本會平行下載。small.js
可能會先完成。
…但 defer
屬性除了告訴瀏覽器「不要阻擋」之外,還會確保保留相對順序。因此,即使 small.js
先載入,它仍會等待並在 long.js
執行後才執行。
當我們需要載入 JavaScript 函式庫,然後載入依賴於它的腳本時,這一點可能很重要。
defer
屬性僅適用於外部腳本如果 <script>
標籤沒有 src
,則會忽略 defer
屬性。
async
async
屬性有點像 defer
。它也會讓腳本變成非阻擋式。但它的行為有一些重要的差異。
async
屬性表示腳本完全獨立
- 瀏覽器不會在
async
腳本上阻擋(就像defer
一樣)。 - 其他腳本不會等待
async
腳本,而async
腳本也不會等待其他腳本。 DOMContentLoaded
和非同步腳本不會互相等待DOMContentLoaded
可能發生在非同步腳本之前(如果非同步腳本在頁面完成後才完成載入)- …或發生在非同步腳本之後(如果非同步腳本很短或在 HTTP 快取中)
換句話說,async
腳本會在背景中載入,並在準備就緒時執行。DOM 和其他腳本不會等待它們,而它們也不會等待任何東西。這是一個完全獨立的腳本,載入後就會執行。這是不是很簡單呢?
以下是類似於我們在 defer
中看到的範例:兩個腳本 long.js
和 small.js
,但現在使用 async
取代 defer
。
它們不會互相等待。無論哪個先載入(可能是 small.js
)都會先執行
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>
<script async src="https://javascriptinfo.dev.org.tw/article/script-async-defer/long.js"></script>
<script async src="https://javascriptinfo.dev.org.tw/article/script-async-defer/small.js"></script>
<p>...content after scripts...</p>
- 頁面內容立即顯示:
async
沒有阻擋它。 DOMContentLoaded
可能發生在async
之前或之後,這裡沒有保證。- 較小的腳本
small.js
排在第二個,但可能在long.js
之前載入,因此small.js
會先執行。不過,如果long.js
有快取,可能會先載入,然後先執行。換句話說,非同步腳本會以「先載入」順序執行。
當我們將獨立的第三方腳本整合到網頁中時,非同步腳本非常有用:例如計數器、廣告等,因為它們不依賴我們的腳本,而我們的腳本也不應該等待它們。
<!-- Google Analytics is usually added like this -->
<script async src="https://google-analytics.com/analytics.js"></script>
async
屬性僅適用於外部腳本。就像 defer
一樣,如果 <script>
標籤沒有 src
,則會忽略 async
屬性。
動態腳本
還有一種重要的方式可以將腳本新增到網頁中。
我們可以使用 JavaScript 建立一個腳本,並將其動態附加到文件中。
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
腳本會在附加到文件後立即開始載入 (*)
。
動態腳本預設會表現為「非同步」。
也就是說
- 它們不會等待任何東西,也沒有任何東西會等待它們。
- 先載入的腳本會先執行(「先載入」順序)。
如果我們明確設定 script.async=false
,則可以變更這個順序。然後,腳本將會按照文件順序執行,就像 defer
一樣。
在此範例中,loadScript(src)
函式會新增一個腳本,並將 async
設定為 false
。
因此,long.js
總是會先執行(因為它是先新增的)。
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
// long.js runs first because of async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");
如果不設定 script.async=false
,則腳本會以預設的先載入順序執行(small.js
可能會先執行)。
同樣地,就像 defer
一樣,如果我們想要載入一個函式庫,然後載入另一個依賴它的腳本,則順序很重要。
摘要
async
和 defer
有一個共同點:下載此類腳本不會阻擋網頁呈現。因此,使用者可以立即閱讀網頁內容並熟悉網頁。
但它們之間也有重要的差異。
順序 | DOMContentLoaded |
|
---|---|---|
async |
先載入順序。它們的文件順序並不重要,先載入的會先執行。 | 無關緊要。即使文件尚未完全下載,它們也可能載入並執行。如果腳本很小或有快取,而文件夠長,就會發生這種情況。 |
defer |
文件順序(按照它們在文件中的順序)。 | 在文件載入並剖析完畢後執行(如果需要,它們會等待),就在 DOMContentLoaded 之前執行。 |
在實務上,defer
用於需要整個 DOM 和/或其相對執行順序很重要的腳本。
而 async
用於獨立的腳本,例如計數器或廣告。而且它們的相對執行順序並不重要。
請注意:如果您使用 defer
或 async
,那麼使用者將在腳本載入之前看到頁面。
在這種情況下,某些圖形元件可能尚未初始化。
別忘了放入「載入中」指示,並停用尚未運作的按鈕。讓使用者清楚地看到他們可以在頁面上執行哪些操作,以及哪些操作仍在準備中。
留言
<code>
標籤,對於多行程式碼 – 請將它們包覆在<pre>
標籤中,對於超過 10 行的程式碼 – 請使用沙盒 (plnkr、jsbin、codepen…)