為什麼要關心載入時機#
要把網頁渲染流程吃透,會發現「什麼時候下載什麼檔案」其實是優化的關鍵。瀏覽器拿到 HTML 後會解析成 DOM、讀取 CSS 變成 CSSOM,兩者結合產生 Render Tree,再算 Layout,最後 Paint 出畫面。這條 Critical Rendering Path 上的每一步都可能因為腳本與資源載入策略不當而被卡住。
JavaScript 的執行依賴 CSSOM 完成,且預設會中斷 HTML 的解析。一個常被提到的小訣竅是:CSS 盡量早引入,JS 放在 CSS 之後再引入,避免 JS 在執行時還要等 CSSOM 準備好。
阻塞與非阻塞 Script#
預設的 script tag 會打斷 HTML 的解析。瀏覽器一遇到它就會立刻下載並執行該腳本,等執行完才繼續往下解析 HTML。
<script src="./main.js"></script>對 script tag 而言還有兩個常被搭配使用的屬性,這也是非阻塞 script 的兩種主要寫法。
async#
async 讓瀏覽器在背景非同步下載腳本,下載完成後馬上執行,執行期間 HTML 解析會暫停。多個 async script 之間沒有保證執行順序,因此適合「不依賴其他模組、不需要操作 DOM」的場景。
最典型的範例就是 Google Analytics 等追蹤工具:載入時機不重要、與業務邏輯沒依賴關係,也不會去動 DOM 結構。
const script = document.createElement("script");
script.src = "/path/to/analytics.js";
document.body.append(script);
// 動態插入的 script 預設是 async,可手動關閉
script.async = false;defer#
defer 同樣是非同步下載,但執行時機固定在 HTML 解析完成之後(DOMContentLoaded 之前)。defer 會保留 script tag 在 HTML 中由上而下的順序,因此即使有依賴關係也能放心使用。對於「不那麼急迫又需要操作 DOM」的腳本,defer 通常是首選。
在不確定要選哪個的時候,先用 defer 比較不會踩雷;async 留給真正獨立、與 DOM 無關的第三方腳本。
什麼是 Resource Hints#
Resource Hints 的精神跟非阻塞 script 類似,都是「我們主動告訴瀏覽器」一些它無法自動推論的資訊,讓它有機會提早下載或建立連線,把延遲攤到使用者察覺不到的時段。
嚴格來說,Resource Hints 指的是 link tag 上帶有特定 rel 屬性的指令。常見的有以下五種:
- preload
- prefetch
- preconnect
- dns-prefetch
- prerender
preload 嚴格來說有自己的 W3C 規格,並非 Resource Hints 的一員,但概念與用途相近,多數討論會把它一起放進來。
無論名字怎麼變,這些指令的核心目標只有一個:對近期會用到的資源預先處理,可能是先下載、可能是先建立連線,等到真正使用時就能省下不少時間。但要記得:用得不對只是浪費頻寬,預先載入了根本用不到的資源反而拖慢效能。
preload 與 prefetch#
兩者經常被搞混,差別其實很清楚:
- preload:用來抓「目前頁面」會立刻用到的關鍵資源。瀏覽器會視為高優先級,幾乎像是命令而非提示。
- prefetch:用來抓「未來頁面」可能用到的資源,瀏覽器會在閒置時段才去處理,可以跨越 navigation。
preload 的特殊之處在於它不像其他 hint 那麼「客氣」。多數 Resource Hints 會被視為低優先級,等瀏覽器有空再處理;preload 則被視為高優先級,瀏覽器必須儘快處理。
as 屬性#
瀏覽器會根據檔案類型決定下載優先級。preload 與 prefetch 都需要透過 as 屬性告訴瀏覽器這是什麼類型的資源,這會直接影響排程。
<link rel="preload" as="font" type="font/woff2" href="myfont.woff2" />加上 preload 之後,這份字型檔的下載優先級會被明顯提升,可以直接在 DevTool 的 Network tab 看到 priority 欄位的變化。
preconnect#
preconnect 等於告訴瀏覽器:「等等會跟某個 domain 要資料,先把連線建好。」這對於跨域資源特別有用。
要理解 preconnect 在做什麼,得先回憶資料傳輸前的步驟:
- 向 DNS 解析網域,拿到 IP
- TCP Handshake
- 如果是 HTTPS,還要做 SSL Negotiation
- 建立完成,等待第一個位元組
每一步都需要一次 RTT(Round Trip Time)。在訊號差的網路下,這幾個 RTT 加起來可能就是好幾百毫秒。preconnect 把 DNS 解析、TCP、SSL 一次處理完,省下完整的三段往返時間。
適合 preconnect 的情境:
- CDN 域名:站內大量資源都從 CDN 拉,提前建好連線就值得。
- 串流媒體:例如 YouTube 的 iframe 預先準備。
- 動態 URL:無法事先列出所有資源,但可以先建立 domain 層級的連線。
瀏覽器只會把 preconnect 的連線保留約 10 秒,超過沒用就會關掉。對於不確定會不會用到的 domain,preconnect 反而是負擔。
dns-prefetch#
dns-prefetch 是 preconnect 的子集,只做第一步 DNS 解析。看起來能力較弱,但正因為它做的事情少、成本低,反而適合「想要做一點優化但又怕浪費頻寬」的情境。
dns-prefetch = DNS lookup
preconnect = DNS lookup + TCP handshake + SSL negotiationMDN 建議只對跨來源(cross-origin)domain 做 dns-prefetch;同源的 IP 早就被解析過,再做一次毫無意義。
兩者也可以一起用,這在實務上很常見:
<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin />
<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />這種寫法是為了照顧 preconnect 支援度較差的瀏覽器:看不懂 preconnect 的瀏覽器會自動忽略它,至少還能透過 dns-prefetch 完成 DNS 解析這一步。
結論:把 preconnect 留給最關鍵的少數連線,其他不確定的就用 dns-prefetch 即可。
prerender#
prerender 比 prefetch 走得更遠。它不只下載目標頁面的資源,還會解析、執行裡面引用的 script,幾乎等於在背景把整個頁面渲染好。等使用者真的點擊跳轉時,頁面就能立刻呈現。
<link rel="prerender" href="next-page.html" />正因為 prerender 連程式碼都會跑,使用上要非常謹慎。只有在「幾乎可以肯定使用者會去」的頁面才適合,例如登入後的主頁。否則代價就是浪費網路頻寬和 CPU 資源。目前 prerender 的支援度不如 prefetch,相容性考量下,prefetch 通常是更穩妥的選擇。
一個小總結#
非阻塞 Script 與 Resource Hints 都是雙面刃。用對地方,使用者完全不會察覺背後的優化;用錯地方,反而把頻寬浪費在用不到的資源上。實務上比較合理的做法是用 JavaScript 動態產生這些 hint,讓相關邏輯集中在同一處檔案裡,後續資源異動時也比較好維護。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10271044