Javascript 腳本:async,defer

2023-02-17 10:55 更新

現(xiàn)代的網(wǎng)站中,腳本往往比 HTML 更“重”:它們的大小通常更大,處理時(shí)間也更長(zhǎng)。

當(dāng)瀏覽器加載 HTML 時(shí)遇到 <script>...</script> 標(biāo)簽,瀏覽器就不能繼續(xù)構(gòu)建 DOM。它必須立刻執(zhí)行此腳本。對(duì)于外部腳本 <script src="..."></script> 也是一樣的:瀏覽器必須等腳本下載完,并執(zhí)行結(jié)束,之后才能繼續(xù)處理剩余的頁面。

這會(huì)導(dǎo)致兩個(gè)重要的問題:

  1. 腳本不能訪問到位于它們下面的 DOM 元素,因此,腳本無法給它們添加處理程序等。
  2. 如果頁面頂部有一個(gè)笨重的腳本,它會(huì)“阻塞頁面”。在該腳本下載并執(zhí)行結(jié)束前,用戶都不能看到頁面內(nèi)容:
<p>...content before script...</p>

<script src="https://javascript.info/article/script-async-defer/long.js?speed=1" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" ></script>

<!-- This isn't visible until the script loads -->
<p>...content after script...</p>

這里有一些解決辦法。例如,我們可以把腳本放在頁面底部。此時(shí),它可以訪問到它上面的元素,并且不會(huì)阻塞頁面顯示內(nèi)容:

<body>
  ...all content is above the script...

  <script src="https://javascript.info/article/script-async-defer/long.js?speed=1" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" ></script>
</body>

但是這種解決方案遠(yuǎn)非完美。例如,瀏覽器只有在下載了完整的 HTML 文檔之后才會(huì)注意到該腳本(并且可以開始下載它)。對(duì)于長(zhǎng)的 HTML 文檔來說,這樣可能會(huì)造成明顯的延遲。

這對(duì)于使用高速連接的人來說,這不值一提,他們不會(huì)感受到這種延遲。但是這個(gè)世界上仍然有很多地區(qū)的人們所使用的網(wǎng)絡(luò)速度很慢,并且使用的是遠(yuǎn)非完美的移動(dòng)互聯(lián)網(wǎng)連接。

幸運(yùn)的是,這里有兩個(gè) <script> 特性(attribute)可以為我們解決這個(gè)問題:defer 和 async。

defer

defer 特性告訴瀏覽器不要等待腳本。相反,瀏覽器將繼續(xù)處理 HTML,構(gòu)建 DOM。腳本會(huì)“在后臺(tái)”下載,然后等 DOM 構(gòu)建完成后,腳本才會(huì)執(zhí)行。

這是與上面那個(gè)相同的示例,但是帶有 defer 特性:

<p>...content before script...</p>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" ></script>

<!-- 立即可見 -->
<p>...content after script...</p>

換句話說:

  • 具有 ?defer? 特性的腳本不會(huì)阻塞頁面。
  • 具有 ?defer? 特性的腳本總是要等到 DOM 解析完畢,但在 ?DOMContentLoaded? 事件之前執(zhí)行。

下面這個(gè)示例演示了上面所說的第二句話:

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" ></script>

<p>...content after scripts...</p>
  1. 頁面內(nèi)容立即顯示。
  2. ?DOMContentLoaded? 事件處理程序等待具有 ?defer? 特性的腳本執(zhí)行完成。它僅在腳本下載且執(zhí)行結(jié)束后才會(huì)被觸發(fā)。

具有 defer 特性的腳本保持其相對(duì)順序,就像常規(guī)腳本一樣。

假設(shè),我們有兩個(gè)具有 defer 特性的腳本:long.js 在前,small.js 在后。

<script defer src="https://javascript.info/article/script-async-defer/long.js" rel="external nofollow"  rel="external nofollow" ></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js" rel="external nofollow"  rel="external nofollow" ></script>

瀏覽器掃描頁面尋找腳本,然后并行下載它們,以提高性能。因此,在上面的示例中,兩個(gè)腳本是并行下載的。small.js 可能會(huì)先下載完成。

……但是,defer 特性除了告訴瀏覽器“不要阻塞頁面”之外,還可以確保腳本執(zhí)行的相對(duì)順序。因此,即使 small.js 先加載完成,它也需要等到 long.js 執(zhí)行結(jié)束才會(huì)被執(zhí)行。

當(dāng)我們需要先加載 JavaScript 庫,然后再加載依賴于它的腳本時(shí),這可能會(huì)很有用。

?defer? 特性僅適用于外部腳本

如果 <script> 腳本沒有 src,則會(huì)忽略 defer 特性。

async

async 特性與 defer 有些類似。它也能夠讓腳本不阻塞頁面。但是,在行為上二者有著重要的區(qū)別。

async 特性意味著腳本是完全獨(dú)立的:

  • 瀏覽器不會(huì)因 ?async? 腳本而阻塞(與 ?defer? 類似)。
  • 其他腳本不會(huì)等待 ?async? 腳本加載完成,同樣,?async? 腳本也不會(huì)等待其他腳本。
  • ?DOMContentLoaded? 和異步腳本不會(huì)彼此等待:
    • ?DOMContentLoaded? 可能會(huì)發(fā)生在異步腳本之前(如果異步腳本在頁面完成后才加載完成)
    • ?DOMContentLoaded? 也可能發(fā)生在異步腳本之后(如果異步腳本很短,或者是從 HTTP 緩存中加載的)

換句話說,async 腳本會(huì)在后臺(tái)加載,并在加載就緒時(shí)運(yùn)行。DOM 和其他腳本不會(huì)等待它們,它們也不會(huì)等待其它的東西。async 腳本就是一個(gè)會(huì)在加載完成時(shí)執(zhí)行的完全獨(dú)立的腳本。就這么簡(jiǎn)單,現(xiàn)在明白了吧?

下面是一個(gè)類似于我們?cè)谥v defer 時(shí)所看到的例子:long.js 和 small.js 兩個(gè)腳本,只是現(xiàn)在 defer 變成了 async。

它們不會(huì)等待對(duì)方。先加載完成的(可能是 small.js)—— 先執(zhí)行:

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>

<script async src="https://javascript.info/article/script-async-defer/long.js" rel="external nofollow"  rel="external nofollow" ></script>
<script async src="https://javascript.info/article/script-async-defer/small.js" rel="external nofollow"  rel="external nofollow" ></script>

<p>...content after scripts...</p>
  • 頁面內(nèi)容立刻顯示出來:加載寫有 ?async? 的腳本不會(huì)阻塞頁面渲染。
  • ?DOMContentLoaded? 可能在 ?async? 之前或之后觸發(fā),不能保證誰先誰后。
  • 較小的腳本 ?small.js? 排在第二位,但可能會(huì)比 ?long.js? 這個(gè)長(zhǎng)腳本先加載完成,所以 ?small.js? 會(huì)先執(zhí)行。雖然,可能是 ?long.js? 先加載完成,如果它被緩存了的話,那么它就會(huì)先執(zhí)行。換句話說,異步腳本以“加載優(yōu)先”的順序執(zhí)行。

當(dāng)我們將獨(dú)立的第三方腳本集成到頁面時(shí),此時(shí)采用異步加載方式是非常棒的:計(jì)數(shù)器,廣告等,因?yàn)樗鼈儾灰蕾囉谖覀兊哪_本,我們的腳本也不應(yīng)該等待它們:

<!-- Google Analytics 腳本通常是這樣嵌入頁面的 -->
<script async src="https://google-analytics.com/analytics.js" rel="external nofollow" ></script>

?async? 特性僅適用于外部腳本

就像 defer 一樣,如果 <script> 標(biāo)簽沒有 src 特性(attribute),那么 async 特性會(huì)被忽略。

動(dòng)態(tài)腳本

此外,還有一種向頁面添加腳本的重要的方式。

我們可以使用 JavaScript 動(dòng)態(tài)地創(chuàng)建一個(gè)腳本,并將其附加(append)到文檔(document)中:

let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)

當(dāng)腳本被附加到文檔 (*) 時(shí),腳本就會(huì)立即開始加載。

默認(rèn)情況下,動(dòng)態(tài)腳本的行為是“異步”的。

也就是說:

  • 它們不會(huì)等待任何東西,也沒有什么東西會(huì)等它們。
  • 先加載完成的腳本先執(zhí)行(“加載優(yōu)先”順序)。

如果我們顯式地設(shè)置了 script.async=false,則可以改變這個(gè)規(guī)則。然后腳本將按照腳本在文檔中的順序執(zhí)行,就像 defer 那樣。

在下面這個(gè)例子中,loadScript(src) 函數(shù)添加了一個(gè)腳本,并將 async 設(shè)置為了 false

因此,long.js 總是會(huì)先執(zhí)行(因?yàn)樗窍缺惶砑拥轿臋n的):

function loadScript(src) {
  let script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.body.append(script);
}

// long.js 先執(zhí)行,因?yàn)榇a中設(shè)置了 async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");

如果沒有 script.async=false,腳本則將以默認(rèn)規(guī)則執(zhí)行,即加載優(yōu)先順序(small.js 大概會(huì)先執(zhí)行)。

同樣,和 defer 一樣,如果我們要加載一個(gè)庫和一個(gè)依賴于它的腳本,那么順序就很重要。

總結(jié)

async 和 defer 有一個(gè)共同點(diǎn):加載這樣的腳本都不會(huì)阻塞頁面的渲染。因此,用戶可以立即閱讀并了解頁面內(nèi)容。

但是,它們之間也存在一些本質(zhì)的區(qū)別:

順序 DOMContentLoaded
async 加載優(yōu)先順序。腳本在文檔中的順序不重要 —— 先加載完成的先執(zhí)行 不相關(guān)??赡茉谖臋n加載完成前加載并執(zhí)行完畢。如果腳本很小或者來自于緩存,同時(shí)文檔足夠長(zhǎng),就會(huì)發(fā)生這種情況。
defer 文檔順序(它們?cè)谖臋n中的順序) 在文檔加載和解析完成之后(如果需要,則會(huì)等待),即在 DOMContentLoaded 之前執(zhí)行。

在實(shí)際開發(fā)中,defer 用于需要整個(gè) DOM 的腳本,和/或腳本的相對(duì)執(zhí)行順序很重要的時(shí)候。

async 用于獨(dú)立腳本,例如計(jì)數(shù)器或廣告,這些腳本的相對(duì)執(zhí)行順序無關(guān)緊要。

沒有腳本的頁面應(yīng)該也是可用的

請(qǐng)注意:如果你使用的是 defer 或 async,那么用戶將在腳本加載完成 之前 先看到頁面。

在這種情況下,某些圖形組件可能尚未初始化完成。

因此,請(qǐng)記得添加一個(gè)“正在加載”的提示,并禁用尚不可用的按鈕。以讓用戶可以清楚地看到,他現(xiàn)在可以在頁面上做什么,以及還有什么是正在準(zhǔn)備中的。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)