4

HTML Script 載入小技巧:defer 與 async

 11 months ago
source link: https://blog.darkthread.net/blog/script-defer-async/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

HTML Script 載入小技巧:defer 與 async

2023-06-11 08:35 AM 0 213

這篇適合技能停在 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 事件發生在最後:

Fig1_638220406409493284.png

<script> 移到網頁最後,便能直接讀取。

Fig2_638220406413018929.png

事實上,早在 HTML4 時代 <script> 便已支援 defer Attribute,連 IE 10+ 都能用,更別說其他真正的瀏覽器。加入 defer 後,不管 <script> 所在位置,瀏覽器會先平行載入外部 js 但不執行,直到 HTML 解析好要觸發 DOMContentLoaded 事件前再執行,與放在 </body> 前方效果相似。

Fig3_638220406414840799.png

我們在 <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 事件前,印證了理論。

Fig4_638220406416703383.png

除了延遲執行,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 載入。

Fig5_638220406418486768.png

先試試 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 載入較久,但仍先執行:

Fig6_638220406420302680.png

換成 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 事件後。

Fig7_638220406422142117.png

若要見證在 async 中斷解析過程執行,要將 DOM 弄複雜一點,塞入三萬顆按鈕。如下圖所示,後載入的 0kb-async.js 在 DOMContentLoaded 之前執行,10mb-async 在 DOMContentLoaded 之後,得證。依據 async 此一特性,它較適合與 DOM 元素無關的 JavaScript 作業,應用時要假設載入順序相反及 DOM 尚未就緒的可能性。

Fig8_638220406424033530.png

學會 defer、async Attriubte,也透過實測觀察其行為,開發前端時可善用它們改善載入效率及增加 Script 安排彈性。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK