網頁效能
本文包含所有網頁效能「概念」,讓你一篇就列舉完不用再去查其他來源。
基礎知識
處理網頁效能問題必須先知道網頁效能瓶頸的特殊性,聽起來很厲害但實際上只是在說網路最大的時間消耗不是實際下載時間而是等待時間,更精細的說法是從你「發送請求到伺服器真正回傳」這段時間都在等待,接收到檔案到下載完成這段時間反而是小頭。
另一點就是網頁效能的基準很看重「用戶首次互動」時間,這代表把所有非首屏內容延遲載入就是所有優化的根本。
為了避免等待,你應該學會分析 devtools waterfall 圖;為了加快首次互動,你應該 inline critical JS/CSS,且保證網頁核心 CSS 足夠小,再把非首屏內容用 JS 延遲載入。
這兩點聽起來很廢話,但是別小看這兩個核心概念,就是一堆網站都沒有做到。
基準
先搞清楚基準才知道要優化什麼,並且還要優化真正有效的基準這些優化才有用,優化不反應實際體驗的基準就失去優化的意義了。2026 現在最重要的基準是最大內容繪製(LCP,largest content paint),他的意思是用戶「初次」進入網站,在「viewport」裡面看到的「最大的內容」的渲染時間,意思是用戶要等多久才會看到主要內容。
LCP 實際內容我就不再寫了,我不會比 Google 本人寫的還好,請見
打包
正確做法是 50KB 內直接全部打包即可,更大就按照功能拆分,大型 vendor JS 則做拆分 chunk。
多檔案打包在 HTTP/1.1 是必要的,因為每個檔案都要佔用一條 TCP 連線,瀏覽器限制約 6 條並行連線。HTTP/2 可多工多個檔案並行傳輸讓打包不再必要,但是打包還是有很多好處,比如:
- 節省每個 request 的 HTTP header
- 字串更多,壓縮效率更高
- 小檔案填不滿一個 TLS Record 造成資源浪費
JS
載入方式
現代已經不再需要為了效能把所有 JS 移動到 body 標籤結束前才載入了,async/defer/type="module" 都可以避免阻塞 HTML 解析並行下載,因此正確的方案是會看到多個 CSS/JS 資源幾乎同時下載,而不是阻塞等待。
defer/type="module" 是並行下載,但是這種載入方式仍然會造成 LCP 問題,原因很簡單,LCP 元素的網路頻寬被搶走了,原本可以全速的網路載入 LCP 元素,現在要和眾多 defer script 分享。這種問題在網速慢的裝置更明顯,並行下載互搶頻寬就造成 LCP 時間變長,簡而言之,這種 LCP 原因對網路越差的環境影響越大。
解決方法也非常直觀易懂,用 JS 動態載入其他非必要 JS。
JS 載入順序問題
本文恐怕又是中文唯一一篇一次統整所有載入順序的文章,我是真查不到。
規則:
- 無屬性 JS 會阻塞 HTML 解析,要等到該 JS 下載和執行完才會繼續解析後續 HTML
- async, defer, type=module 屬性會和 HTML 並行下載
- async 屬性的 JS 下載完成後立刻執行,不等待 HTML 是否完成解析
- defer, type=module 屬性並行下載,等到 HTML 解析完成後才立刻執行
- defer, type=module 的執行順序按照 HTML 上下順序排列,會被放到是同一個 queue,越前面的越早執行
- defer, type=module 的 fetchpriority 本來就是 low,加了也沒用
- 動態插入的 JS 標籤只要插入就會執行、請求網路,規則和上述相同
結論就是:
- 永遠不要用無屬性的 JS,這應該 inline 到 HTML 裡面
- async 的使用場景極度侷限,不如用 defer
- 現代 ESM 腳本只能用 type=module 載入
- 如果 ESM 腳本需要用到只有 UMD 的套件庫,將該套件庫在 HTML 的 ESM 腳本之前,使用 defer 載入就會按照順序執行
圖示如下:
CSS
總建議就是把所有東西都打包成唯一一個 CSS,除非你的網站 CSS 很大超過 100 KB,不過個人網站應該不會出現這種問題。最常造成效能的問題只有在 head 以外的地方載入 CSS 導致 CSSOM 需要修改,因此效能扣分。
設定 media="print" onload="this.media='all'" 可以非同步載入樣式,雖然能上場的地方不多,但是能上場時是真的很有用,簡單有效 (Resource inlining in JavaScript frameworks)。
關鍵 CSS
關鍵 CSS 壓在 14KB 以內可以在 TCP initcwnd 第一次傳輸就直接把數據一起傳完,不需要等 ACK 回應,但是實際上很少看到有人做關鍵 CSS 拆分。
圖片
WebP 是 21 世紀的神,永遠該用他,傳統 JPG/PNG 要等到完全下載之後才能解析圖片,WebP 不只壓縮率高,更可以下載到一半就開始解析圖片,各種硬體裝置也都支援良好,其他次世代格式 APNG/JXL/AVIF 全都輸在支援度上1。
你應該在上傳圖片前就先在本地將圖片轉好檔而不是透過 Hugo 幫你轉,否則每次 CI 都在浪費資源,本地轉好也就不需要處理 Cloudflare 上 Hugo 糟糕的支援度,連設定快取都困難重重。
你可以參考我的工作流程:使用 automator 簡化日常工作流程,或是使用開源專案 FotoKilof 也能輕鬆轉檔。
屬性設定
fetchpriority— 調整瀏覽器對已知資源的下載優先級high:LCP hero image、carousel 第一張、async 但關鍵的 scriptlow:carousel 後續圖、非關鍵 preload、背景 API fetch
loading— 控制是否延遲載入eager(預設):fold 以上圖片,尤其 LCP 圖絕對不能加lazylazy:fold 以下圖片,節省初始頻寬- 非常有用,明顯有感,務必記得設定
decoding— 控制圖片解碼是否阻塞主執行緒auto(預設):多數情況夠用async:非關鍵圖片,讓解碼不卡主執行緒sync:需要立即呈現的圖片
<link rel="preload">— 讓 parser 提早發現資源- 不要濫用。preload 是強制 fetch,多加反而搶頻寬,已在 HTML 直接引用的資源不需要 preload
- 用於 JS 引入的資源、CSS background LCP image、字體、dynamic import 的依賴
- 搭配
fetchpriority="high"提升 preload 的優先級(預設 preload 不一定高)
fetchpriority 請見 Optimize resource loading with the Fetch Priority API,AI 給的建議通常都是錯的,這個 API 比他外表還複雜的多。
srcset
Preload 圖片
是否在 head 加上 <link rel="preload" as="image" href="..."> 是比較常見的錯誤因此提升到二級標題,這裡講的是 preload 圖片,因此討論的是首屏的 LCP 圖片問題,我的看法是靜態網站 preload 首屏圖片對沒什麼幫助。
首先搞懂 preload 設計目的是用於較難發現的資源(fonts included in stylesheets, background images, or resources loaded from a script),他讓瀏覽器更早發現你的資源且強制載入,但是
- 瀏覽器本身就有 Preload Scanner 這個機制,不等 DOM 建構就會自動判斷資源優先級並且馬上發出請求
- CSS/JS 本來就是最重要,最該先載入的東西
- 那麼再來才是你的首屏圖片
- 可是首屏圖片也不會離 head 多遠
- 那麼是否在 head 加上 preload 的延遲也就是幾百 byte 而已
- 實際測試就是下載開始時間差了約 40 ms
結論是可以做但影響不大,但是如果你用 CSS/JS 而不是 img 標籤載入背景圖片,這時候使用 preload 就很有用了。
- Optimize resource loading with the Fetch Priority API
- Optimize Largest Contentful Paint To ensure your LCP resource starts loading as early as possible, it's critical that the resource is discoverable in the initial HTML document response by the browser's preload scanner... 後略,這個意思就是 browser 自己的 preload scanner 就已經可以偵測到 img 標籤的 LCP 了,下面要你 preload 的是因為資源藏在 CSS/JS 裡面所以才需要手動指定 preload,反過來說純 HTML 圖片不需要自己設定 preload
- How To Preload Your Largest Contentful Paint Image 說了一樣的話:如果最大內容繪製(LCP)圖片是從另一個檔案中引用的,這種方式就無法運作。
- Don't fight the browser preload scanner
- Harry Roberts - Optimising Largest Contentful Paint background-image 對 LCP 很差
Speculation Rules
告訴瀏覽器預取和預渲染:預取就是進入 viewport 就會自動請求連結的資源,預渲染就是連渲染都幫你做好了,點進連結就秒切,設定方式很簡單,把這段放到 body 標籤結束前即可:
<script type="speculationrules">
{
"prefetch": [
{
"where": { "href_matches": "/*" },
"eagerness": "eager"
}
],
"prerender": [
{
"where": { "href_matches": "/*" },
"eagerness": "moderate"
}
]
}
</script>
Web Performance 2025: The Shift from Optimization to Prediction
HTTP 103 Early Hints
這是很強大的功能,但是 SSG 網站不適用。目的在伺服器還在後台處理、生成 HTML 時,先回傳一個 early hints 告訴客戶端會用到 CSS 你先下載,利用這個等待的時間差提前下載關鍵資源。
不過 SSG 的網站通常沒有伺服器,也就沒辦法動態處理 response,或是用 Cloudflare Worker 完成,如 CF 的 103 文檔範例所示。
一點補充
在基準裡面的官方文件就已經寫的很清楚了,這裡補充一點小知識
- LCP 和 DomcontentLoaded 都是性能指標,但是 DomcontentLoaded 是被淘汰的那個,因為不真正反應用戶體驗
- LCP/DomcontentLoaded 沒有一定誰前誰後,請見你还傻傻分不清楚 FP、FCP、LCP、DCL(DOMContentLoaded) 、L(Load) 出现的时机吗?
- 做效能優化第一步就是先把目標功能直接關了,看看沒有這功能速度差多少,你才知道問題根源在哪裡,而且這個差異就是你能做到的優化上限
- fetchpriority 只是建議瀏覽器,不是指令,請見 Optimize resource loading with the Fetch Priority API
- 一堆 vibe coding 蠢人看到 preload/fetchpriority 就亂加一通,實際上是瀏覽器比你想的更聰明,瀏覽器的 preload scanner 機制在解析 HTML 的同時就幫每個資源安排好優先級了,如果不用什麼 CSS background-image 基本上不需要寫 rel=preload,亂寫通常是把效能搞的更糟,請見 Don't fight the browser preload scanner
- 比如說 site logo 放 preload 根本毫無意義,他本來就在頁面最上方了。真正要做的是縮圖,或是直接改 svg。你可以觀察大公司的網站,幾乎無一例外都用 img 標籤 + svg logo。
- 動態載入資源對 LCP 絕對是大大幫助。