[CODE]

在靜態 Astro 頁面上追逐 LCP 與 CLS

兩天的 Core Web Vitals 優化:移除造成 CLS 0.19 的浮水印腳本、Hero 圖的 AVIF vs WebP LCP 取捨、用 mask-image 處理顆粒紋理、以及用 critical CSS 把行動版 Lighthouse 推到約 92。

1 min read
astro performance core-web-vitals cloudflare css

我花了兩天把 /intro 頁面從「還可以」拉到行動版 Lighthouse 約 92 分,而大部分工作其實是在拆掉我自己為了美觀或「安全」加進去的東西。有趣的不是最後的分數,而是這些提升有多少來自刪程式碼。

浮水印腳本毀了 CLS

最糟的元兇是 security.js 裡一個叫 addWatermarkToImages 的函式。它在每張圖載入後執行、插入 wrapper 元素、改 inline style。結果:CLS 0.19,而且很尷尬地浮水印上印錯了品牌名。我直接整個移除(7b4bde91)。沒有版面位移、沒有錯誤 logo、JS 也更少。這東西沒有任何值得保留的版本——在靜態行銷頁上用前端 DOM 做浮水印是演戲,不是安全。

security.js 其餘部分也一直跟我作對。它做 inline style 變動拖長了 LCP,又在 idle 時對整份文件跑 querySelectorAll。我把整支腳本延後到 load 事件,讓它碰不到關鍵路徑(8cdd5ef8a8bfffd6),接著直接拿掉那個 idle 掃描(ad882242)。一個不斷被驗證的經驗法則:任何在首次繪製後變動 DOM 的東西,在被證明無害前都是 CLS/LCP 的負債。

Hero 圖:AVIF 輸了

Hero 一開始是 CSS background-image,這是經典的 LCP 錯誤——瀏覽器無法對背景圖做優先排程。我改成真正的 <picture>fetchpriority="high" 與 480w 變體(bf77b9c2)。

接著是 AVIF 的問題。我有 AVIF + WebP 兩種來源,AVIF 比較小。但對 LCP 元素本身 來說,行動裝置上解碼成本比傳輸大小更重要。AVIF 的解碼延遲把繪製推得比稍大的 WebP 還晚。我把 AVIF 從 Hero 的 <picture> 拿掉,只留 WebP(1c4c3c69)。AVIF 在折線下方的海報圖仍然勝出,因為那裡解碼時間看不見——這是針對關鍵路徑上唯一那個元素的精準決定。

我也在 decoding 上來回試。試了 decoding="sync" 想讓 LCP 對齊 FCP(b94d73b6),又改回 auto(65579657),因為 sync 只是阻塞繪製,並沒有實質提前 LCP 時間戳。兩個 commit 才確認預設值是對的——這就是用量測取代猜測的代價。

顆粒紋理,試了三次

body::before 的顆粒疊層是真正的 LCP 拖累,因為它是個 background-image data URI,瀏覽器會早早抓取並繪製。我先換成 inline SVG filter(c9860dab),再從 background-image 改成 mask-image(6f6a162c),讓顆粒變成在純色上的合成層效果,而非繪製時的紋理。最後在 intro 頁上,我乾脆把折線上方的顆粒整個移除(a2b57c3a)——在真正有流量的頁面上,它不值得一毫秒的 LCP。

延後第三方 JS

還有兩個關鍵路徑元兇:gtag/js 和又一次的 security.js。兩者都移到 load 事件(938f240e)。分析腳本沒理由在 LCP 繪製前執行。事後看很明顯,但預設的 GA 片段會注入到 <head>,大家就放著不動。

Critical CSS,以及它重新引入的 CLS

最後一步是 inline 折線上方的 critical CSS 並延後完整樣式表(a2b57c3a)。這是標準招數,但它立刻咬了我:我第一次抽出的 critical CSS 不完整,折線上方的元素用部分樣式渲染,然後延後的 CSS 一到就位移——這是我在追 LCP 時自己製造的 CLS 退步。我得回頭把折線上方規則補完,讓 inline 那組完整描述首次繪製時所有可見的東西(2795739e)。Critical CSS 只有完整時才是贏面;不完整的那組只是把繪製延遲換成版面位移。

快取 HTML

唯一純基礎設施的勝利:在 middleware 設 s-maxage=3600(ddf3bea9),讓 Cloudflare 在邊緣快取渲染好的 HTML。這些是靜態行銷頁,沒理由每個請求都打到 origin。用 s-maxage(僅共享快取)而非 max-age,避免瀏覽器過度快取,同時讓 CDN 服務重複請求。

總結

88 → 約 92 行動版平均(3f081fde)。帳面上不多。但拆解才是重點:單一最大的修正是刪掉浮水印腳本,而非任何聰明的優化。我大部分的 LCP 提升都是移除東西——AVIF 解碼、background-image 顆粒、background-image hero、阻塞 head 的分析腳本——這些都是我沒量測過它們在關鍵路徑上代價就加進去的。那些聰明的部分(mask-image、critical CSS)收益更小也更冒險,而 critical CSS 那個在幫上忙之前還先反咬了一口。