我持續組織了近一年的源碼共讀活動,感興趣的可以?點此掃碼加我微信?ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列。另外:目前建有江西|湖南|湖北
籍前端群,可加我微信進群。

前言
在面試的時候,經常會遇到一道經典的面試題:
如何優化網頁加載速度?
常規的回答中總會有一條:
把 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>
他的執行順序是:
在控制臺打印:
Howdy ~
請求并執行 vue.global.js
請求并執行 vue-router.global.js
在頁面中展示:
Hello 👋🏻 ~
觸發 DOMContentLoaded[1] 事件

瀏覽器的解析規則是:如果遇到 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 個屬性:defer
和 async
,就是為了解決此類問題,提升頁面性能的。
<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>
他的執行順序是:
在控制臺打印:
Howdy ~
在頁面中展示:
Hello 👋🏻 ~
請求并執行 vue.global.js
請求并執行 vue-router.global.js
觸發 DOMContentLoaded[3] 事件

如果在 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,起到降級的作用。
注意:
defer
屬性僅適用于外部腳本,如果script
腳本沒有src
,則會忽略defer
特性。defer
屬性對模塊腳本(script type='module'[4])無效,因為模塊腳本就是以defer
的形式加載的。
<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>
他的執行順序是:
在控制臺打印:
Howdy ~
并行請求 analytics.js 和 ad.js
在頁面中展示:
Hello 👋🏻 ~
根據網絡的實際情況,以下幾項會無序執行
執行 analytics.js(下載完后,立即執行)
執行 ad.js(下載完后,立即執行)
觸發 DOMContentLoaded 事件(可能在在上面 2 個腳本之前,之間,之后觸發)

瀏覽器在解析到帶有 async
屬性的 script
標簽時,也不會阻塞頁面,同樣是在后臺默默下載此腳本。當他下載完后,瀏覽器會暫停解析 HTML,立馬執行此腳本。
看起來還是蠻好理解的吧?咱們再來討論 2 個小細節:
Q1:如果設置了 async
屬性的 script
下載完之后,瀏覽器還沒解析完 HTML,會怎樣?
A1:瀏覽器會暫停解析 HTML,立馬執行此腳本,等執行完之后,再繼續解析 HTML。
Q2:如果有多個 async
屬性的 script
標簽,那等他們下載完成之后,會按照代碼順序執行嗎?
A2:不會。執行順序是:誰先下載完成,誰先執行。async
的特點是「完全獨立」,不依賴其他內容。
最佳實踐:
當我們的項目,需要集成其他獨立的第三方庫時,可以使用此屬性,他們不依賴我們,我們也不依賴于他們。通過設置此屬性,讓瀏覽器異步下載并執行他,是個不錯的優化方案。
注意:
async
特性僅適用于外部腳本,如果script
腳本沒有src
,則會忽略async
特性。
總結
defer
不阻塞瀏覽器解析 HTML,等解析完 HTML 之后,才會執行
script
。會并行下載 JavaScript 資源。
會按照 HTML 中的相對順序執行腳本。
會在腳本下載并執行完成之后,才會觸發
DOMContentLoaded
事件。在腳本執行過程中,一定可以獲取到 HTML 中已有的元素。
defer
屬性對模塊腳本無效。適用于:所有外部腳本(通過
src
引用的script
)。
async
不阻塞瀏覽器解析 HTML,但是
script
下載完成后,會立即中斷瀏覽器解析 HTML,并執行此script
。會并行下載 JavaScript 資源。
互相獨立,誰先下載完,誰先執行,沒有固定的先后順序,不可控。
由于沒有確定的執行時機,所以在腳本里面可能會獲取不到 HTML 中已有的元素。
DOMContentLoaded
事件和script
腳本無相關性,無法確定他們的先后順序。適用于:獨立的第三方腳本。
另外:async
和 defer
之間最大的區別在于它們的執行時機。
One More Thing
你有沒有想過,如果一個 script
標簽同時設置 defer
和 async
,瀏覽器會如何處理?
先說結論:從表現形式上來說,async
的優先級比 defer
高,也就是如果同時存在這 2 個屬性,那么瀏覽器將會以 async
的特性去加載此腳本。
這主要分 2 種情況:
如果是「普通腳本」,瀏覽器會優先判斷async
屬性是否存在,如果存在,則以async
特性去加載此腳本,如果不存在,再去判斷是否存在defer
屬性。
如果是「模塊腳本[5]」,瀏覽器會判斷async
屬性是否存在:
如果存在,瀏覽器會并行下載此模塊和他的所有依賴模塊,等全部下載完成之后,會立刻執行此腳本。
如果不存在,瀏覽器也會并行下載此模塊和他的所有依賴模塊,然后等瀏覽器解析完 HTML 之后,再執行此腳本。
另外需要注意的是:在模塊腳本上設置
defer
屬性是無效的。
一圖勝千言
最后,用一張圖概括一下這兩個屬性的加載模式吧:

思考題 🤔
為什么瀏覽器在解析到普通的 script 標簽時,必須先執行他?
普通的 script 標簽會阻塞瀏覽器解析 HTML,這會導致什么問題?
本文首發于:https://github.com/mrlmx/blogs/issues/4 ,如果喜歡,記得去點個贊哦~ 👍 ??
參考
https://javascript.info/script-async-defer[6]
https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html[7]
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script[8]
https://html.spec.whatwg.org/multipage/scripting.html[9]
相關鏈接
[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
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助5000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 lxchuan12、拉你進源碼共讀群
今日話題
目前建有江西|湖南|湖北?籍 前端群,想進群的可以加我微信 lxchuan12?進群。分享、收藏、點贊、在看我的文章就是對我最大的支持~