Why you need JavaScript defer and async attribute

從動圖了解如何有效的使用 defer 和 async 屬性加載網頁中的外部腳本

發現問題

<body>
<!-- 網頁內容 -->
<script src="script.js"></script>
<!-- 網頁內容 -->
</body>

問題一:剛進入網頁就讓瀏覽器花許多時間下載與解析腳本:如果在網頁中加入腳本的話,會導致網頁的渲染需要花更長的時間去完成,這是因為瀏覽器在載入腳本的時候會停止渲染網頁,直到載入並執行完成才會繼續。網頁渲染到一半被擱置,對使用者來說是極差的體驗。

<p>第一個元素</p>
<script>
console.log(document.querySelectorAll('p')); // NodeList [ p ]
</script>
<!-- 上面的腳本選取不到之後的 DOM 內容 -->
<p>第二個元素</p>

問題二:腳本之後渲染的 DOM 元素會選取不到:如果腳本中有選取 DOM 元素的動作,但是 DOM 元素還沒有被建構出來,就會出現選取不到的問題。

解決方法

現今的瀏覽器都有支援 HTML 內建的 defer🔗async🔗 屬性,這兩個屬性都可以讓瀏覽器在載入腳本的時候不會停止渲染網頁,它們的差異主要在於執行時機。底下會介紹三種方法,分別是不使用任何屬性、使用 defer 屬性、使用 async 屬性。

解決方法一:把腳本放在底部

<body>
<h1>網頁內容</h1>
<script src="script.js"></script>
</body>

這是早期常見也最簡單粗暴的做法,但這樣會導致需要等到整個網頁渲染完才能開始加載腳本,在特長的 HTML 文件或緩慢的網路速度下容易拖慢腳本被執行的速度。

解決方法二:defer

<head>
<script src="script.js" defer></script>
</head>
<body>
<p>第一個元素</p>
<p>第二個元素</p>
</body>

defer 就是東西先下載,但晚點再執行的概念,這樣做我們可以在網頁的開頭(通常會在<head>中)就向外抓取腳本資源,並在 DOM 渲染完畢時才執行。

解決方法三:async

<script src="script.js" async></script>

總結與補充

在大多場合選擇任一個屬性都會對網頁的載入速度有所改善,但是如果腳本之間有相依性,就需要特別注意了。

defer 在 DOMContentLoaded 事件之後嗎?

「DOMContentLoaded」事件表示 HTML 文件已經完全被載入並被解析。對於「defer」腳本,它可以創建/刪除 DOM 元素,因此「DOMContentLoaded」事件只會在所有「defer」腳本執行完成後觸發,以確保在「defer」腳本進行所有可能的 DOM 更新後,最終 DOM 樹狀結構已經準備好。

如果同時添加 defer 與 async 會發生什麼事?

<script src="script.js" async defer></script>

第二屬性會被作為備用屬性,如果瀏覽器不支援第一項屬性,就會使用第二項屬性。舉例來說,如果瀏覽器不支援 async,就會使用 defer,如果瀏覽器不支援 defer,就會使用預設的行為。

參考資料