HTML Script 載入小技巧:defer 與 async
1 |
這篇適合技能停在 jQuery 及 WebForm + AJAX 時代的老人。
依我從小學到的傳統概念,要存取 DOM 元素必須把程式寫進 $(function() ) 或 window.onload 事件(二者差別在於前者發生在載入 DOM 後,後者需等圖檔等資源載入完成)以確保程式執行時 DOM 已經載入。(延伸閱讀 使用 jQuery(document).ready() 與 window.onload 注意事項 by 保哥)。到了 AJAX 時代則流行另一種做法,將 Script 放在網頁末端如 </body>
的前方,執行至此 DOM 元素已解析完畢可直接存取。ASP.NET WebForm 為此提供了一個 API:RegisterStartuoScript 可將 Script 區塊放在 </form>
前,也是相同概念。
用 jQuery 包裝或掛載 DOMContentLoaded、window.onload 事件會讓程式巢狀化,結構變得複雜;限定 script 放到網頁尾端則限制了彈性,不利將 js 集中在 head 統一管理。但如果不這麼做,在 DOM 還沒解析完成前執行,存取對象會不存在。
這裡用個範例展示。demo.js 如下,目標是要讀取 <div id="d">
文字,測試四種方式:直接讀取、window.onload 事件、jQuery(document).read()、jQuery(function() ) (快捷寫法,與 jQuery(document).read() 作用相同)。
function readDomElement(label) {
console.log('%c' + label, 'color: cyan');
if (!document.body)
console.log(' No document.body');
else
console.log(' #d textContent = ' + document.getElementById('d').textContent);
}
readDomElement('direct access');
window.addEventListener('load', function() {
readDomElement('window.onload');
});
$(document).ready(() => readDomElement('jQuery document ready()'));
$(function() {
readDomElement('jQuery(function() ...)');
});
第一次測試將 <script src="demo.js"></script>
放在 <head>
區:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="demo.js"></script>
</head>
<body>
<div id="d">DEMO</div>
</body>
</html>
結果如預期,直接讀取會撲空(因為瀏覽器還沒讀到 <body>
內容),其餘三者成功,而 windows.onload 事件發生在最後:
將 <script>
移到網頁最後,便能直接讀取。
事實上,早在 HTML4 時代 <script>
便已支援 defer Attribute,連 IE 10+ 都能用,更別說其他真正的瀏覽器。加入 defer 後,不管 <script>
所在位置,瀏覽器會先平行載入外部 js 但不執行,直到 HTML 解析好要觸發 DOMContentLoaded 事件前再執行,與放在 </body>
前方效果相似。
我們在 <script src="demo.js"></script>
加上 defer,並加掛 DOMContentLoaded 事件,證明 defer 的執行時機在其之前。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="demo.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function(event) {
console.log('%c DOMContentLoaded', 'background: #222; color: #bada55');
});
</script>
</head>
<body>
<div id="d">DEMO</div>
</body>
</html>
如此,即使 <script src="demo.js" defer></script>
放在 <head>
區,直接存取 DOM 也沒問題,而發生時間則在 DOMContentLoaded 事件前,印證了理論。
除了延遲執行,defer 跟另一個 async Attribute 還有個效果是,可在解析 DOM 過程平行載入,讓瀏覽器不會卡在大型 js 下載,早點顯示頁面。defer 與 async 的差別在於 defer 會等到 DOMContentLoaded 事件前才執行,async 則是解析完就立即執行。
理論是理論,用實驗來見證吧!
我用以下 PowerShell 程式產生特肥的 .js 檔:
param (
[Parameter(Mandatory=$true)][string]$name,
[Parameter(Mandatory=$true)][int]$sizeInMB
)
$file = "$name.js"
'/*' | Out-File $file -Encoding utf8
(1..1024*4*$sizeInMB) | ForEach-Object {
"0123456789ABCDEF" * 16 | Out-File $file -Append -Encoding utf8
}
'*/' | Out-File $file -Append -Encoding utf8
"console.log('$name', document.getElementById('d')?.textContent);" | Out-File $file -Append -Encoding utf8
總共做了 2 個 10MB、2 個 1MB,準備分別用 defer 跟 async 載入。
先試試 defer:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="10mb-defer.js" defer></script>
<script src="1mb-defer.js" defer></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('%c DOMContentLoaded', 'background: #222; color: #bada55');
});
</script>
</head>
<body>
<div id="d">DEMO</div>
</body>
</html>
defer 保證執行順序,10MB 載入較久,但仍先執行:
換成 async 試試:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('%c DOMContentLoaded', 'background: #222; color: #bada55');
});
</script>
<script src="10mb-async.js" async></script>
<script src="1mb-async.js" async></script>
</head>
<body>
<div id="d">DEMO</div>
</body>
</html>
async 是載入完成就執行,10MB 排前面,但 1MB 體積小下載快,反而先執行。照理 async 若在 DOM 解析過程完成,可中斷 DOM 解析先執行,但也可能在 DOMContentLoaded 之後才跑,這個案例由於 .js 偏肥,都發生在 DOMContentLoaded 事件後。
若要見證在 async 中斷解析過程執行,要將 DOM 弄複雜一點,塞入三萬顆按鈕。如下圖所示,後載入的 0kb-async.js 在 DOMContentLoaded 之前執行,10mb-async 在 DOMContentLoaded 之後,得證。依據 async 此一特性,它較適合與 DOM 元素無關的 JavaScript 作業,應用時要假設載入順序相反及 DOM 尚未就緒的可能性。
學會 defer、async Attriubte,也透過實測觀察其行為,開發前端時可善用它們改善載入效率及增加 Script 安排彈性。
Introduction to Script defer and async attributes.
Comments
# by 大人
ASP.NET WebForm 為此提供了一個 API:RegisterStartuoScript 可將 Script 區塊放在 .... 错别字RegisterStartupScript