PageSpeed 93 → 83 又修回:typewriter 動畫遇到 LCP 元素的真實 debug
為了讓 Lab 有點動感 我加了一個 hero 文字逐字 fade-in 的 typewriter 進場動畫。結果 PageSpeed 行動裝置從 93 掉到 83、LCP 從 2.3s 拖到 4.0s(會不會太離譜。
5 分鐘 debug 找到 root cause:typewriter 把 hero h1 設成 opacity:0 等 IntersectionObserver 觸發再 fade in 0.45s。hero h1 = LCP 元素。LCP 計算「pixel 達最終值」的時間。opacity 0 → 1 全程都算延遲。
拿掉 typewriter 後 perf 回到 90+。這篇紀錄完整 debug 過程 + 教訓 + 替代方案。給加了 JS 動畫後 perf 掉分不知為何的非工程師。
1. 我加了什麼動畫
想做的效果:hero h1「AI SEO 新手成長」進頁面時 文字一個一個跳出來 像 AI 對話的打字機。
技術實作:
- 寫一個 typewriter.js 在 DOMContentLoaded 後 把 hero h1 內容 wrap 成 per char
- 每個 span CSS opacity:0 + translateY(8px) + animation-delay 遞增
- 用 IntersectionObserver 觀察 hero h1 → viewport 內 add .is-visible class 觸發 animation
- 考量到 hero h1 內含 包不下去 改用 element-level fade-in (opacity 0 → 1 + transition 0.45s)
效果上 hero h1 進場有「淡入感」OK 但有個副作用我沒想到 — 這個淡入會被 PageSpeed 算到 LCP。
2. PageSpeed 報告(加 typewriter 後)
改後:效能 83、FCP 2.2s 橘、LCP 4.0s 紅、TBT 100ms 綠、CLS 0.059 綠。
比起加 typewriter 前(效能 93、LCP 2.3s) LCP 多了 1.7 秒 整體效能掉了 10 分。
3. 5 分鐘 debug:LCP 元素是什麼
PageSpeed 報告往下滑會有「最大內容繪製元素」這個區塊 — 直接告訴你哪個元素是頁面 LCP 候選。
我的 hero h1「AI SEO 新手成長」是 LCP 元素。檢查它的 CSS:
.typewriter-fade {
opacity: 0;
transform: translateY(4px);
transition: opacity 0.45s ease-out;
}
.typewriter-fade.is-visible {
opacity: 1;
transform: translateY(0);
}
問題在這裡。LCP 是 Largest Contentful Paint — Google 計算「最大元素達最終 pixel 值」的時間。
Hero h1 在 typewriter 接管後的時序:
- 頁面 load 完 hero h1 文字在 DOM 但 opacity:0(看不見)
- 等 DOMContentLoaded + JS init(~200-300ms)
- IntersectionObserver callback 觸發 add .is-visible class
- transition opacity 0 → 1 過 0.45s
- 最終 hero h1 pixel 達 opacity:1 → LCP 時刻
= LCP 多出 ~1.5-2 秒 vs 沒 typewriter 直接 paint 出來。
4. 為什麼 JS 動畫遇到 LCP 元素要小心
LCP 是 Core Web Vitals 三大指標之一(LCP / INP / CLS)。Google 把它當搜尋排名訊號 — LCP > 4s 差、2.5-4s 中、< 2.5s 好。
JS 動畫常見會踩到 LCP 的 3 個坑:
- opacity 0 → 1 fade-in — LCP 算「最終 pixel」 fade 過程都不算 done
- transform translate / scale 進場 — 同上 final transform 達成才算 done
- JS 延遲注入內容 — 例如 React 渲染前的 placeholder 也會被算 LCP 直到真實內容 mount
對「想看動畫」vs「LCP」這對矛盾 通常選 LCP — perf 是搜尋排名直接訊號 動畫只是 nice-to-have。
5. 拿掉 typewriter 完整 step(5 分鐘)
- 刪 assets/typewriter.js 檔案
- 拿掉 build.py 內 TYPEWRITER_JS_V 變數 +