script 標籤的 defer 與 async 屬性

前言

在面試的時候,經常會遇到一道經典的面試題:

如何優化網頁加載速度?

常規的回答中總會有一條:

把 css 文件放在頁面頂部,把 js 文件放在頁面底部。

那麼,爲什麼要把 js 文件放在頁面的最底部呢?

我們先來看下這段代碼:

<!DOCTYPE html>
<html lang="zh">
  <head>
    <title>Hi</title>
    <script>
        console.log("Howdy ~");
    </script>
    <script src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script>
    <script src="https://unpkg.com/vue-router@4.1.5/dist/vue-router.global.js"></script>
  </head>
  <body>
    Hello 👋🏻 ~
  </body>
</html>

他的執行順序是:

script 加載邏輯

瀏覽器的解析規則是:如果遇到 script 標籤,則暫停構建 DOM,轉而開始執行 script 標籤,如果是外部 script[2],那麼瀏覽器還需要一直等待其「下載」並「執行」後,再繼續解析後面的 HTML。

如果請求並執行「vue.global.js」需要 3 秒,「vue-router.global.js」需要 2 秒,那麼頁面中的 Hello 👋🏻 ~,則至少需要 5 秒以上纔會展示出來。

可以看到,script 標籤會阻塞瀏覽器解析 HTML,如果把 script 都放在 head 中,在網絡不佳的情況下,就會導致頁面長期處於白屏狀態。

在很久以前,一般都是將這些外聯腳本,放在 body 標籤的最後面,確保先解析展示 body  中的內容,然後再一個個請求執行這些外聯腳本。

那有沒有其他更優雅的解決方案呢?

答案是肯定的,現在 script  標籤新增了 2 個屬性:deferasync,就是爲了解決此類問題,提升頁面性能的。

<script defer>

先看一下 MDN 上的解釋:

這個布爾屬性被設定用來通知瀏覽器該腳本將在文檔完成解析後,觸發 DOMContentLoaded 事件前執行。

有 defer 屬性的腳本會阻止 DOMContentLoaded 事件,直到腳本被加載並且解析完成。

文檔是直接總結了他的特性,我們先看看下面的代碼,展開說說細節,加深一下理解。

<!DOCTYPE html>
<html lang="zh">
  <head>
    <title>Hi</title>
    <script>
      console.log("Howdy ~");
    </script>
    <script defer src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script>
    <script defer src="https://unpkg.com/vue-router@4.1.5/dist/vue-router.global.js"></script>
  </head>
  <body>
    Hello 👋🏻 ~
  </body>
</html>

他的執行順序是:

script defer 加載邏輯

如果在 script 標籤上設置了 defer 屬性,那麼在瀏覽器解析到這裏時,會默默的在後臺開始下載此腳本,並繼續解析後面的 HTML,並不會阻塞解析操作。

等到 HTML 解析完成之後,瀏覽器會立即執行後臺下載的腳本,腳本執行完成之後,纔會觸發 DOMContentLoaded 事件。

看起來還是蠻好理解的吧?咱們再來討論 2 個小細節:

Q1: 如果 HTML 解析完成之後,設置了 defer 屬性的腳本還沒下載完成,會怎樣?

A1: 瀏覽器會等腳本下載完成之後,再執行此腳本,執行完成之後,再觸發 DOMContentLoaded 事件。

Q2: 如果有多個設置了 defer 屬性的腳本,那瀏覽器會如何處理?

A2: 瀏覽器會並行的在後臺下載這些腳本,等 HTML 解析完成,並且所有腳本下載完成之後,再按照他們在 HTML 中出現的相對順序執行,等所有腳本執行完成之後,再觸發 DOMContentLoaded 事件。

最佳實踐:

建議所有的外聯腳本都默認設置此屬性,因爲他不會阻塞 HTML 解析,可以並行下載 JavaScript 資源,還可以按照他們在 HTML 中的相對順序執行,確保有依賴關係的腳本運行時,不會缺少依賴。

在 SPA 的應用中,可以考慮把所有的 script 標籤加上 defer 屬性,並且放到 body 的最後面。在現代瀏覽器中,可以並行下載提升速度,也可以確保在老瀏覽器中,不阻塞瀏覽器解析 HTML,起到降級的作用。

注意:

<script async>

按照慣例,先看一下 MDN 上的解釋:

對於普通腳本,如果存在 async 屬性,那麼普通腳本會被並行請求,並儘快解析和執行。

對於模塊腳本,如果存在 async 屬性,那麼腳本及其所有依賴都會在延緩隊列中執行,因此它們會被並行請求,並儘快解析和執行。

該屬性能夠消除解析阻塞的 Javascript。

解析阻塞的 Javascript 會導致瀏覽器必須加載並且執行腳本,之後才能繼續解析。

感覺這段描述的已經蠻清晰了,不過咱們還是先看看下面的代碼,展開說說細節,加深一下理解。

<!DOCTYPE html>
<html lang="zh">
  <head>
    <title>Hi</title>
    <script>
      console.log("Howdy ~");
    </script>
    <script async src="https://google-analytics.com/analytics.js"></script>
    <script async src="https://ads.google.cn/ad.js"></script>
  </head>
  <body>
    Hello 👋🏻 ~
  </body>
</html>

他的執行順序是:

script async 加載邏輯

瀏覽器在解析到帶有 async 屬性的 script 標籤時,也不會阻塞頁面,同樣是在後臺默默下載此腳本。當他下載完後,瀏覽器會暫停解析 HTML,立馬執行此腳本。

看起來還是蠻好理解的吧?咱們再來討論 2 個小細節:

**Q1:**如果設置了 async 屬性的 script 下載完之後,瀏覽器還沒解析完 HTML,會怎樣?

**A1:**瀏覽器會暫停解析 HTML,立馬執行此腳本,等執行完之後,再繼續解析 HTML。

**Q2:**如果有多個 async 屬性的 script 標籤,那等他們下載完成之後,會按照代碼順序執行嗎?

**A2:**不會。執行順序是:誰先下載完成,誰先執行。async 的特點是「完全獨立」,不依賴其他內容。

最佳實踐:

當我們的項目,需要集成其他獨立的第三方庫時,可以使用此屬性,他們不依賴我們,我們也不依賴於他們。通過設置此屬性,讓瀏覽器異步下載並執行他,是個不錯的優化方案。

注意:

總結

defer

async

另外:asyncdefer 之間最大的區別在於它們的執行時機。

One More Thing

你有沒有想過,如果一個 script 標籤同時設置 deferasync,瀏覽器會如何處理?

先說結論:從表現形式上來說,async 的優先級比 defer 高,也就是如果同時存在這 2 個屬性,那麼瀏覽器將會以 async 的特性去加載此腳本。

這主要分 2 種情況:

如果是「普通腳本」,瀏覽器會優先判斷async屬性是否存在,如果存在,則以async特性去加載此腳本,如果不存在,再去判斷是否存在defer屬性。

如果是「模塊腳本 [5]」,瀏覽器會判斷async屬性是否存在:

一圖勝千言

最後,用一張圖概括一下這兩個屬性的加載模式吧:

defer 和 async 的加載模式

思考題 🤔

本文首發於:https://github.com/mrlmx/blogs/issues/4 ,如果喜歡,記得去點個贊哦~ 👍 ❤️

參考

相關鏈接

[1]

DOMContentLoaded: https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event

[2]

外部 script: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-src

[3]

DOMContentLoaded: https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event

[4]

script type='module': https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type

[5]

模塊腳本: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type

[6]

https://javascript.info/script-async-defer: https://javascript.info/script-async-defer

[7]

https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html: https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html

[8]

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script

[9]

https://html.spec.whatwg.org/multipage/scripting.html: https://html.spec.whatwg.org/multipage/scripting.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8dboFtZJFMW4TQQb2AWkwg