返回部落格

動畫網站的 Core Web Vitals 優化:保留動效,透過 LCP、INP 和 CLS

動畫網站的 Core Web Vitals 優化:保留動效,透過 LCP、INP 和 CLS

2026年10月6日 — 效能

發布於

2026年10月6日

閱讀時間

5 分鐘

分類

效能

標籤

  • Core Web Vitals
  • Performance
  • Animation
  • WebGL
  • LCP
  • INP
  • CLS

(摘要)如何在保留捲動動畫、WebGL 和影片的同時透過 Core Web Vitals:LCP、INP、CLS 變慢的具體原因和解決方法。

動畫網站常被認為很慢,而且很多時候確實如此。但動效和良好的 Core Web Vitals 並不矛盾。大多數問題都來自一小串原因,而每個原因都有能保留動畫的解決辦法。

本站有影片首屏、WebGL 專案牆、WebGL 文字和貫穿全站的捲動動畫。下面是它遵循的規則。

三個指標,簡單說

指標 衡量什麼 良好標準
LCP — 最大內容繪製 主要內容何時出現 ≤ 2.5 秒
INP — 互動到下一次繪製 頁面對點擊和觸控的響應速度 ≤ 200 毫秒
CLS — 累積佈局偏移 佈局跳動的程度 ≤ 0.1

Google 以真實 Chrome 使用者的第 75 百分位來評判(即 PageSpeed Insights 和 Search Console 中的實地資料),而不是你在筆記本上跑的一次 Lighthouse。在一臺中端安卓手機上、用限速網路測試,這更接近你的第 75 百分位使用者,而不是 MacBook。

LCP:儘早顯示真實內容

不要把首屏藏在入場動畫後面

動畫網站最常見的 LCP 殺手,是首屏從 opacity: 0 開始、等 JavaScript 載入後再淡入。元素可見之前,瀏覽器不會把它算作已繪製,於是 LCP 要先等你的 JS 包,再等動畫。

解決方法:用 HTML 和 CSS 直接渲染首屏的最終狀態。讓次要元素做入場動畫,或者從一個已經可見的狀態開始動畫(輕微的縮放或位移,而不是從透明度 0 開始)。

給 LCP 元素一張海報圖和高優先順序

如果首屏是影片或 WebGL 畫布,LCP 候選元素通常是它的海報圖。讓這張圖儘量小、提前預載入、高優先順序:

html
<link rel="preload" as="image" href="/hero/poster.avif" fetchpriority="high" />
<img src="/hero/poster.avif" alt="" fetchpriority="high" width="1600" height="900" />

在海報圖繪製之後再啟動影片或 WebGL 場景,並淡入切換。

需要時再載入重型庫

如果 Three.js、GSAP 外掛和 Lottie 驅動的是首屏以下的區塊,它們就不必出現在首個 JS 包裡。在區塊接近視口時動態匯入:

ts
const observer = new IntersectionObserver(async ([entry]) => {
  if (!entry.isIntersecting) return
  observer.disconnect()
  const { initScene } = await import('./scene')
  initScene(canvas)
}, { rootMargin: '400px 0px' })
observer.observe(section)

在 Next.js 中,next/dynamic 配合 ssr: false 可以對整個元件做同樣的事。

提供合適尺寸的媒體

在手機上播放 1440p 的循環影片是浪費頻寬,還會和 LCP 圖片搶資源。按視口寬度和網路情況選擇影片源,首屏循環影片保持簡短(6–10 秒,不帶音軌)。

INP:保持主執行緒空閒

INP 衡量從互動到下一幀之間的延遲。如果 JavaScript 每一幀都很忙,動畫就會拖累 INP。

只對 transform 和 opacity 做動畫

它們由合成器處理,不會觸發重排。對 top、left、width、height 或 margin 做動畫,每一幀都會強制重新佈局:

css
/* Slow: layout every frame */
.card { transition: top 0.3s; }

/* Fast: compositor only */
.card { transition: transform 0.3s; }

不要在捲動回呼裡讀取佈局

樣式變化之後呼叫 getBoundingClientRect() 會強制同步佈局。在捲動回呼中,每幀只在 requestAnimationFrame 裡讀一次,然後再寫:

ts
let ticking = false
window.addEventListener('scroll', () => {
  if (ticking) return
  ticking = true
  requestAnimationFrame(() => {
    const top = section.getBoundingClientRect().top   // read
    progressBar.style.transform = `scaleX(${clamp(-top / range)})` // write
    ticking = false
  })
}, { passive: true })

更好的做法是讓 GSAP 的 ScrollTrigger 之類的庫幫你批次處理,或者在支援的瀏覽器裡使用 CSS 捲動驅動動畫(animation-timeline: scroll())。

停止看不見的工作

頁面上的每個 requestAnimationFrame 循環都在主執行緒上執行。畫布、影片和循環離開視口時,以及標籤頁被隱藏時,都要暫停:

ts
const io = new IntersectionObserver(([e]) => (e.isIntersecting ? start() : stop()))
io.observe(canvas)
document.addEventListener('visibilitychange', () => (document.hidden ? stop() : start()))

限制畫布解析度

手機上 devicePixelRatio 為 3 的 WebGL 畫布,渲染的像素是 DPR 1 的九倍。限制它:

ts
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

對於霧、噪點這類全螢幕效果,用一半解析度渲染再放大,通常肉眼看不出區別。

拆分長任務

建置場景、解析大型 JSON 或準備大量文字網格,可能會阻塞主執行緒幾百毫秒。把工作拆成小塊,塊與塊之間讓出主執行緒(支援的瀏覽器用 await scheduler.yield(),否則用 setTimeout(0)),這樣使用者在初始化期間的點擊也能被快速響應。

CLS:為所有東西預留空間

給媒體設定尺寸

每張圖片、每個影片、iframe 和 canvas 都需要 width/height 屬性或 CSS aspect-ratio,讓瀏覽器在載入前就預留空間。

載入字型時不要跳動

展示字型是動畫網站個性的一部分,而字型晚到導致的替換會讓每一行都發生偏移。預載入一兩個關鍵字型檔案、做子集化,並使用 font-display: swap 加一個調整過度量參數的後備字型(size-adjust、ascent-override)——next/font 會自動生成這些。

動畫不算,佈局變化才算

CLS 忽略 transform 動畫,但不會忽略透過佈局屬性改變的尺寸和位置。載入後把內容往下推的橫幅會被計入;同樣的橫幅如果在預留好的空間裡用 transform 滑入,就不會。

尊重「減少動態效果」設定

有些訪客在作業系統裡要求減少動態效果。請尊重這個設定——這是無障礙要求,而且這些訪客還能得到更輕的頁面:

css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

在 JavaScript 中,啟動循環、視差和自動播放影片之前,先檢查 matchMedia('(prefers-reduced-motion: reduce)')。

能發現效能回退的工作流程

  1. 在 Chrome DevTools 中開啟 4 倍 CPU 降速,捲動整個頁面並錄製 Performance 記錄。找長任務和紫色的佈局條。
  2. 開啟 Rendering → Paint flashing 和 Layout Shift Regions,看哪些區域在重繪和移動。
  3. 上線後每週查看 PageSpeed Insights 的實地資料;實驗室分數只是參考,實地資料才是成績。

檢查清單

  • 首屏內容在初始 HTML 中可見,而不是從透明淡入
  • LCP 圖片使用 fetchpriority="high" 預載入
  • 重型庫按需載入
  • 只對 transform 和 opacity 做動畫
  • 佈局讀取在 requestAnimationFrame 中批次進行
  • 螢幕外的循環、畫布和影片已暫停
  • 限制了畫布像素比
  • 所有媒體都有尺寸;字型有度量匹配的後備字型
  • 尊重 prefers-reduced-motion

動畫實現方面,請看 在 React 和 Next.js 中使用 GSAP。

電子報

用 Email 訂閱新文章

有新文章時寄一封簡短的信給你。不發垃圾信,隨時可以取消訂閱。

在 React 和 Next.js 中使用 GSAP:useGSAP、清理與 ScrollTrigger
在 React 和 Next.js 中使用 GSAP:useGSAP、清理與 ScrollTrigger
下一篇