那個房型頁分數卡在七十幾分,TBT 高到 3,336ms——而照我自己寫的程式碼註解,這頁根本不該下載任何一個影片位元組。
VideoThumb.astro 裡的註解講得很白:「Lighthouse 不會捲動,所以影片位元組永遠不會被抓。」這句話是錯的,而且已經錯了一陣子。把它為什麼錯查清楚,就是這篇的全部。
這四頁為什麼重要
這是一個更大系統裡的一個節點。這家民宿的直訂通路靠 Google Ads 把付費流量送進四個到達頁,而每一個訪客都是在被節流的手機網路上進來的。Google 的品質分數——決定客戶每次點擊要付多少錢——會直接讀到達頁的速度。慢頁不只是體驗問題,它是這檔廣告每一次點擊都要繳的稅,而且永遠繳下去。
客戶的報價裡寫明 Lighthouse 手機 ≥ 90。所以需求很具體:四頁、手機、≥ 90,LCP 低於 2.5 秒,TBT 低於 200ms。這四頁是首頁、介紹頁、交通頁,還有單一房型頁——漏斗最深、也最慢的那一頁。
沒有延後到任何東西的延後
原本的 gtag.js 載入器做的是教科書寫法:先同步塞一個 gtag stub,讓點擊處理器持續把事件排進 dataLayer,再用 requestIdleCallback 載入真正的腳本,讓它在 TTI 之後才跑、不會墊高 TBT。在真實瀏覽器裡,這沒問題。idle 回呼會等到主執行緒沒有更該做的事才動。
Lighthouse 的模擬節流不是這樣運作的。模擬器在初次解析後幾乎立刻就進入「idle」——沒有真正的互動,沒有真實的 CPU 爭用讓主執行緒忙著。所以 requestIdleCallback 在解析完就立刻觸發,把真正的 gtag.js 拉進網路,剛好就在 hero 圖最需要頻寬的那個窗口。
數字攤開來看:Google Ads 標籤 140KB,GA4 子腳本再 177KB,合計 317KB。在模擬的慢速 4G 上,這些頻寬直接跟 hero AVIF 搶,把 LCP 從大約 1 秒推到大約 3 秒。idle 回呼策略沒有降低成本,它只是把成本搬到時間軸上最糟的位置。
修法是別再把 idle 當訊號,改綁一個模擬器會認帳的事件:window.load 再加 1,500ms 延遲。這保證 LCP 繪製完成之後,那 317KB 才開始下載。同步 stub 和 dataLayer 重播完全不動,所以不會掉任何一個轉換事件——佇列只是晚一點才排空而已。
IntersectionObserver 得的是同一種病
影片縮圖是更大的兇手,根本原因卻是同一個錯覺。原本的程式用 IntersectionObserver 配 rootMargin: '200px 0px',在影片捲動接近視窗時自動靜音播放。註解背後的推理是:Lighthouse 不會捲動,所以 observer 不會觸發,所以影片不會下載。
但 Lighthouse 手機視窗高 812px,200px 的 root margin 把觸發區延伸到足夠遠,遠到影片縮圖在載入時就已經在區域裡了——根本不用捲動。observer 在第一幀就觸發。這啟動了兩個影片檔的預載,在模擬慢速 4G 上合計大約 17MB。光這一筆下載,就是整個 3,336ms 的 TBT。
這裡讓人意外的不是延後載入會失準。是兩個完全不同的延後機制——一個靠 idle,一個靠視窗——在同一個模擬器底下、因為同一個底層原因,往同一個方向壞掉:模擬器對「idle」和「在視窗外」的模型,跟真實的瀏覽 session 不一樣。一個資深工程師讀到原本那兩段註解,多半會點頭認同;兩個說法聽起來都對。
我把 observer 換成單純的點擊播放。訪客沒點海報之前,一個位元組都不抓。順帶一個好處:點擊本身就是使用者手勢,所以 video.play() 在第一次互動就算不靜音也能放——這是自動播放路線拿不到的乾淨手勢,因為瀏覽器在載入時會拒絕不靜音的自動播放。
真正關於 LCP 而不是 TBT 的兩個改動
把搶頻寬的兩個兇手清掉之後,兩個比較小的改動收尾。
房型模板加了一個 heroPreload 提示,在 HTML 解析階段就 emit 一個 <link rel="preload" as="image" type="image/avif">,讓 preload scanner 在排版引擎發現 LCP 圖之前就開始抓。那個檔名 stem 是把 hero 圖路徑的副檔名剝掉算出來的,所以不管內容集合指向哪張圖,它都會跟著對齊。
交通頁原本有一個 Google Maps iframe,在頁面一開始就載入它的 JavaScript。我換成點擊載入的 facade——一個有地標圖示的樣式化佔位框,只有被點時才把真正的 iframe 換進來。大多數來看民宿位置的訪客要的是地址,不是互動地圖;要地圖的人按需求拿。Google Maps 的 JS 現在完全離開關鍵路徑。
最後落點
| 頁面 | 分數 | LCP | TBT |
|---|---|---|---|
| 房型頁 | 97 | 1.9s | 180ms |
| 首頁 | 98 | 1.6s | 130ms |
| 介紹頁 | 100 | 1.1s | 60ms |
| 交通頁 | 100 | 1.2s | 90ms |
四頁全部過 ≥ 90,LCP 低於 2.5 秒,TBT 低於 200ms。誠實的但書:這些是本機跑的,沒有真實網路節流、也沒有真實 CPU 節流,所以對比 PageSpeed Insights 會偏樂觀一點。房型頁的 180ms TBT 就壓在 200ms 線下面,在 PSI 的真實條件下可能會翻過去——這該在上線後盯著看,而不是現在就宣布完成。
我會帶走的一句:延後策略的好壞,取決於這個 runtime 怎麼定義「待會」,而 Lighthouse 對「待會」的定義,有時候就是「現在」。