EP.32Web Dev Bootcamp

Web 效能優化:
Core Web Vitals 與 Lighthouse 實戰

LCP、INP、CLS 三大指標 — 從分析到實際改善,讓你的 Google Search 評分達到 90+

Joseph Chen 2026 16 min read LCP · INP · CLS · Lighthouse · Next.js

Section 1:為什麼效能很重要(數據說話)

Web 效能不只是技術問題,它直接影響業務成果。以下是幾個有名的數據,可以幫助你在團隊中說服非技術的利益相關人為何值得投資效能優化。

Google 研究

每增加 1 秒

轉換率下降 7%

行動版網頁載入時間與轉換率的直接關聯研究

Amazon

每 100ms 延遲

損失 1% 銷售額

早在 2006 年就發現的規律,至今仍被廣泛引用

BBC

每增加 1 秒

損失 10% 用戶

行動版新聞網站用戶留存率與速度的相關性

Core Web Vitals 是 Google 排名因素之一(2021 年起)

Google 在 2021 年 6 月正式將 Core Web Vitals 納入搜尋排名演算法。在內容品質相近的情況下,效能更好的網頁會獲得更高的排名。這讓效能優化從「加分項」變成了「必要條件」。

LCP目標 < 2.5s

最大內容渲染時間

衡量載入速度

Largest Contentful Paint

INP目標 < 200ms

互動反應速度

衡量互動性

Interaction to Next Paint

CLS目標 < 0.1

版面位移量

衡量視覺穩定性

Cumulative Layout Shift

INP 在 2024 年 3 月取代了 FID

舊的 FID(First Input Delay)只量測第一次互動的延遲,而 INP 量測整個頁面生命週期中所有互動的反應速度,更能反映用戶的實際體驗。如果你在看舊文章,FID 已經不再是 Core Web Vitals 的一部分。

Section 2:LCP(Largest Contentful Paint)— 讓頁面快速顯示主要內容

🟢

< 2.5s

良好

🟡

2.5s — 4s

需改善

🔴

> 4s

LCP 量測的是頁面上「視口中最大的內容元素」完成渲染的時間。通常是 Hero 圖片、大標題(H1)或背景圖。改善 LCP 的本質是:讓瀏覽器盡快取得這個元素所需的資源。

問題 1:圖片沒有優化

LCP 元素最常見的是圖片。使用 Next.js 的 Image 組件,你幾乎可以自動解決圖片相關的所有 LCP 問題。

tsx
// ❌ 原生 img 標籤:沒有優化,沒有 lazy loading 控制
<img src="/hero.jpg" width={1200} height={600} alt="Hero image" />
// 問題:
// - 沒有自動轉換為 WebP/AVIF(更小的檔案大小)
// - 沒有根據螢幕尺寸提供不同大小的圖片(srcset)
// - 沒有 blur placeholder(圖片載入前一片白)
// - 沒有 lazy loading 控制(所有圖片都立刻載入)

// ✅ Next.js Image 組件:自動處理所有圖片優化
import Image from 'next/image';

// LCP 元素必須加 priority!
// priority 告訴瀏覽器:這是最重要的圖片,立刻預載入(preload)
// 沒有 priority 的圖片預設是 lazy load(出現在視口才載入)
<Image
  src="/hero.jpg"
  width={1200}
  height={600}
  priority                     // LCP 元素必備!移除 lazy loading,加入 preload
  placeholder="blur"           // 載入期間顯示模糊的 placeholder
  blurDataURL="data:image/..."  // 可以省略,Next.js 自動產生
  sizes="(max-width: 768px) 100vw, 1200px"  // 告訴瀏覽器在不同螢幕寬度下的顯示尺寸
  alt="Hero image"
  className="w-full h-auto object-cover"
/>

// sizes 的寫法解釋:
// "(max-width: 768px) 100vw" → 手機上佔全部視口寬度
// "1200px"                   → 桌面上最大 1200px
// 瀏覽器用這個資訊決定要下載哪個尺寸的圖片(自動選最合適的)

問題 2:字型阻塞渲染

Google Fonts 的傳統 <link> 標籤需要建立額外的 HTTP 連線,字型未載入完成前文字渲染可能被阻塞,造成 Flash of Invisible Text(FOIT)。

tsx
// ❌ 傳統 Google Fonts link 標籤(在 layout.tsx 的 head 中)
// <link rel="preconnect" href="https://fonts.googleapis.com" />
// <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
// <link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet" />
// 問題:
// - 需要額外 2-3 次 DNS + TCP + TLS 連線
// - 字型檔案另外下載,可能延遲數百毫秒
// - 字型換頁時重新下載(沒有 self-hosting)

// ✅ next/font/google:字型在 build time 下載到本地
import { Inter, Noto_Sans_TC } from 'next/font/google';

// 英文字型
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // 先顯示 fallback 字型,字型載入後替換(無 FOIT)
  variable: '--font-inter',  // CSS variable,可以在 Tailwind 中使用
});

// 中文字型(注意 preload 可能很大,建議不設或只選需要的 weight)
const notoSansTC = Noto_Sans_TC({
  subsets: ['chinese-traditional'],
  weight: ['400', '500', '700'],  // 只下載需要的字重
  display: 'swap',
  variable: '--font-noto',
});

// 在 layout.tsx 的 body 加上 class
export default function RootLayout({ children }) {
  return (
    <html lang="zh-TW">
      <body className={`${inter.variable} ${notoSansTC.variable} font-sans`}>
        {children}
      </body>
    </html>
  );
}

問題 3:Server Response 太慢(TTFB 高)

TTFB(Time to First Byte)高表示伺服器處理時間太長。LCP 不可能在 TTFB 之前完成,所以 TTFB 是 LCP 的下限。

使用 CDN / Edge Network

  • Vercel 自動將靜態內容部署到全球 Edge 節點
  • Cloudflare Pages 同樣效果
  • 用戶從最近的伺服器取得內容,TTFB 從 300ms 降到 10ms

加資料庫索引

  • 找出慢查詢(slow query log)
  • 在 WHERE 條件的欄位加索引
  • 善用 ORM 的 explain() 分析查詢計畫

使用 ISR / SSG 預渲染

  • 靜態頁面從 CDN 直接回傳,TTFB < 50ms
  • ISR 讓你有定期更新的靜態頁面
  • 動態部分用 Suspense streaming

Section 3:INP(Interaction to Next Paint)— 互動要快速

🟢

< 200ms

良好

🟡

200ms — 500ms

需改善

🔴

> 500ms

INP 量測從用戶互動(點擊、鍵盤輸入、觸碰)到瀏覽器完成下一次繪製的時間。如果 INP 高,用戶會感覺頁面「卡卡的」— 按了按鈕沒反應、輸入文字有延遲。根本原因通常是 JavaScript 主線程被阻塞。

問題 1:JavaScript 主線程阻塞

瀏覽器的 JavaScript 主線程同時負責執行 JS 和繪製畫面。如果有昂貴的同步運算佔用主線程,用戶互動的回應就會被延遲。

tsx
// ❌ 昂貴的同步運算阻塞 UI
function handleSearch(query: string) {
  // 假設 hugeDataset 有 10 萬筆資料
  // 複雜的 filter 可能執行 100ms+,這段時間 UI 完全凍結
  const results = hugeDataset.filter((item) =>
    item.title.toLowerCase().includes(query.toLowerCase()) &&
    item.tags.some(tag => tag.startsWith(query))
  );
  setResults(results);
}

// ✅ 使用 startTransition 區分高/低優先級更新
import { startTransition, useState } from 'react';

function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
    const newQuery = e.target.value;

    // 高優先級:輸入框的值立刻更新(不延遲,確保打字流暢)
    setQuery(newQuery);

    // 低優先級:搜尋結果可以稍後更新
    // React 會先完成高優先級的更新,再處理 transition 內的更新
    // 如果用戶繼續打字,舊的 transition 會被中止(Concurrent Mode)
    startTransition(() => {
      const filtered = hugeDataset.filter((item) =>
        item.title.toLowerCase().includes(newQuery.toLowerCase())
      );
      setResults(filtered);
    });
  }

  return (
    <>
      <input value={query} onChange={handleSearch} placeholder="搜尋..." />
      <ResultList results={results} />
    </>
  );
}

問題 2:大型 Component 的重新渲染

tsx
// ✅ useDeferredValue — 延遲非關鍵 UI 更新
import { useDeferredValue, useMemo, useState } from 'react';

function SearchResults({ query }: { query: string }) {
  // deferredQuery 會比 query 落後一個渲染周期
  // 輸入框更新立刻觸發,搜尋結果更新在瀏覽器空閒時才執行
  const deferredQuery = useDeferredValue(query);

  // 使用 deferredQuery 而不是 query 來計算結果
  // 這樣即使 query 快速變化,昂貴的 useMemo 也不會每次都跑
  const results = useMemo(
    () => performExpensiveSearch(deferredQuery),
    [deferredQuery]
  );

  // 可以利用 query !== deferredQuery 判斷是否正在更新
  const isUpdating = query !== deferredQuery;

  return (
    <div style={{ opacity: isUpdating ? 0.7 : 1, transition: 'opacity 0.2s' }}>
      {results.map((item) => (
        <ResultItem key={item.id} item={item} />
      ))}
    </div>
  );
}

// useDeferredValue vs startTransition 的差異:
// - startTransition:你控制「哪個 setState 是低優先級」(push 模型)
// - useDeferredValue:你控制「哪個值應該延遲更新」(pull 模型)
// - 如果值來自父組件 props,只能用 useDeferredValue

問題 3:事件 Handler 太慢

tsx
// ✅ 把昂貴的計算移到 Web Worker(完全不阻塞主線程)
// 適合:圖片處理、資料解析、複雜的數學運算

// worker.ts
self.onmessage = (e) => {
  const { data } = e;
  // 在 worker 中執行昂貴操作(不阻塞主線程)
  const result = performHeavyComputation(data);
  self.postMessage(result);
};

// 在 React 組件中使用 Web Worker
function HeavyComputationComponent() {
  const workerRef = useRef<Worker | null>(null);

  useEffect(() => {
    workerRef.current = new Worker(new URL('./worker.ts', import.meta.url));
    workerRef.current.onmessage = (e) => {
      setResult(e.data);  // 收到結果後更新 UI
    };
    return () => workerRef.current?.terminate();
  }, []);

  function handleCompute(input: unknown) {
    // 發送到 worker 執行,主線程立刻回傳,UI 不卡頓
    workerRef.current?.postMessage(input);
  }

  return <button onClick={() => handleCompute(bigData)}>開始計算</button>;
}

// ✅ 使用 requestAnimationFrame 分批處理
function processLargeBatch(items: unknown[]) {
  let index = 0;

  function processBatch() {
    const batchSize = 100;  // 每幀處理 100 筆
    const end = Math.min(index + batchSize, items.length);

    for (let i = index; i < end; i++) {
      processItem(items[i]);
    }

    index = end;

    if (index < items.length) {
      // 讓瀏覽器有機會繪製下一幀,然後繼續處理
      requestAnimationFrame(processBatch);
    }
  }

  requestAnimationFrame(processBatch);
}

Section 4:CLS(Cumulative Layout Shift)— 防止頁面跳動

🟢

< 0.1

良好

🟡

0.1 — 0.25

需改善

🔴

> 0.25

CLS 量測頁面生命週期中意外的版面位移總量。當用戶正在點擊某個按鈕,結果按鈕突然跑掉、點到了別的東西 — 這就是 CLS 造成的糟糕體驗。CLS 的分數是所有位移事件的加總(位移幅度 × 受影響的視口比例)。

問題 1:圖片沒有設定尺寸(最常見)

tsx
// ❌ 圖片沒有尺寸:圖片載入後突然佔了空間,把下面的內容往下推
// CLS 分數可能因此暴增
<img src="photo.jpg" alt="用戶照片" />
// 圖片下載完成前,這個 <img> 的高度是 0
// 下載完成後,假設圖片 400px 高,整個下方內容被推下去 400px
// 這就是一次嚴重的 layout shift

// ✅ 方法 1:設定明確的 width 和 height
// 瀏覽器知道圖片的比例後,會預先保留空間
<img src="photo.jpg" width={800} height={600} alt="用戶照片" />

// ✅ 方法 2:CSS aspect-ratio(更彈性,適合響應式)
<div style={{ aspectRatio: '16 / 9', background: '#f0f0f0' }}>
  <img src="video-thumbnail.jpg" className="w-full h-full object-cover" alt="縮圖" />
</div>

// ✅ 方法 3:使用 Next.js Image(自動計算 aspect ratio)
import Image from 'next/image';

<Image
  src="photo.jpg"
  width={800}
  height={600}
  alt="用戶照片"
  // Next.js Image 自動根據 width/height 保留空間,CLS = 0
/>

問題 2:字型替換導致文字位移

css
/* 問題:Web Font 載入時,fallback 字型(系統字型)和 web font 的字元寬度不同
   文字會在字型替換時跳動(造成 CLS)

   解法:size-adjust 讓 fallback 字型的大小接近 web font */

/* 分析你的 web font 和 system font 的差異,找到合適的 size-adjust 值 */
@font-face {
  font-family: 'CustomFontFallback';
  src: local('Arial');           /* 使用系統字型作為 fallback */
  ascent-override: 90%;          /* 調整上升高度 */
  descent-override: 22%;         /* 調整下降高度 */
  line-gap-override: 0%;
  size-adjust: 107%;             /* 關鍵:讓 fallback 字型大小接近 web font */
}

body {
  font-family: 'CustomFont', 'CustomFontFallback', Arial, sans-serif;
  /* 字型替換時,因為大小已經調整過,位移量大幅減少 */
}

/* 使用 next/font 時 Next.js 會自動計算這些值 */
/* 這是 next/font 自動 zero CLS 的秘密 */

問題 3:動態注入的內容(廣告、Banner)

tsx
// ❌ 廣告/Banner 在頁面渲染後才插入,把下方內容往下推
<ArticleContent />
<AdBanner />  {/* 廣告資料從 API 取得後才插入,造成 layout shift */}
<MoreContent />

// ✅ 預先保留廣告空間(Skeleton)
function AdPlaceholder() {
  return (
    // 給廣告預留固定高度的空間
    // 廣告載入後填入這個空間,不影響下方內容
    <div
      className="w-full bg-gray-100 rounded-lg flex items-center justify-center"
      style={{ minHeight: '250px' }}  // 標準廣告高度
    >
      <AdBanner />  {/* 廣告放在這個固定大小的容器內 */}
    </div>
  );
}

// ✅ 動態通知 Toast(從固定位置出現,不推動其他內容)
// 使用 fixed/absolute positioning 讓 Toast 不佔 layout 空間
function Toast({ message }: { message: string }) {
  return (
    <div className="fixed bottom-4 right-4 bg-black text-white px-4 py-3 rounded-lg shadow-lg z-50">
      {/* fixed 定位不影響文件流,不會造成 CLS */}
      {message}
    </div>
  );
}

問題 4:動畫導致位移(用 transform 而非 margin)

css
/* ❌ 改變 layout 屬性的動畫 — 觸發 Layout + Paint,影響其他元素 */
.slide-in-bad {
  animation: slideInBad 0.3s ease;
}

@keyframes slideInBad {
  from { margin-left: -300px; }  /* margin 影響 layout,推動周圍元素 */
  to   { margin-left: 0; }
}

/* ❌ 同樣有問題的屬性:top、left、width、height、padding */
/* 這些屬性的變化都會觸發 Layout Recalculation,影響 CLS */

/* ✅ 只改變視覺位置,不影響 layout 的動畫 */
.slide-in-good {
  animation: slideInGood 0.3s ease;
}

@keyframes slideInGood {
  from { transform: translateX(-300px); opacity: 0; }  /* transform 不影響 layout */
  to   { transform: translateX(0); opacity: 1; }
}

/* ✅ 高效能動畫的黃金法則:只動 transform 和 opacity */
/* transform:不觸發 Layout 或 Paint(GPU 加速)*/
/* opacity:不觸發 Layout(但觸發 Paint,仍然很快)*/

/* ✅ 加上 will-change 讓瀏覽器提前準備 GPU 層 */
.animated-element {
  will-change: transform;  /* 提示瀏覽器此元素會動,提前建立 compositing layer */
  /* 注意:不要對所有元素都加,只用在確實會動的元素上 */
}

Section 5:Next.js 效能最佳實踐清單

使用 Next.js 開發時,框架本身提供了許多效能優化工具。以下是一份可以立刻套用到你的專案的最佳實踐清單,每一項都有對應的實作方式。

1. Image 優化完整設定

tsx
import Image from 'next/image';

// LCP 元素(Hero 圖片、主要內容圖)
<Image
  src="/hero.jpg"
  width={1200}
  height={600}
  priority                          // 移除 lazy loading,加入 preload link
  placeholder="blur"                // 圖片載入期間顯示模糊版本
  blurDataURL="/hero-blur.jpg"      // 小尺寸的 base64 圖片(或讓 Next.js 自動產生)
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 1200px"
  alt="Hero 圖片"
  className="w-full h-auto"
/>

// 一般內容圖片(預設 lazy load)
<Image
  src={post.coverImage}
  width={800}
  height={450}
  // 不加 priority → 預設 lazy load,出現在視口前不下載
  placeholder="blur"
  sizes="(max-width: 768px) 100vw, 800px"
  alt={post.title}
/>

// 使用者頭像(固定小尺寸)
<Image
  src={user.avatar}
  width={40}
  height={40}
  className="rounded-full"
  alt={user.name}
/>

2. Dynamic Import — 程式碼分割

Dynamic Import 讓你把大型組件分割成獨立的 JS chunk,只有在需要時才下載。這直接減少初始頁面的 JS 大小,改善 LCP 和 INP。

tsx
import dynamic from 'next/dynamic';

// ✅ 大型圖表庫(不需要在首屏顯示)
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => (
    <div className="w-full h-64 bg-gray-100 rounded-xl animate-pulse" />
  ),
  ssr: false,  // 圖表通常只在客戶端渲染(需要 DOM API)
});

// ✅ 富文本編輯器(只在用戶點擊編輯時才載入)
const RichTextEditor = dynamic(() => import('./RichTextEditor'), {
  loading: () => <p>載入編輯器中...</p>,
  ssr: false,
});

// ✅ 模態視窗(只在開啟時才需要)
const VideoModal = dynamic(() => import('./VideoModal'), {
  ssr: false,
});

function ProductPage() {
  const [showEditor, setShowEditor] = useState(false);
  const [showVideo, setShowVideo] = useState(false);

  return (
    <div>
      {/* 點擊才載入編輯器,節省初始頁面的 JS */}
      <button onClick={() => setShowEditor(true)}>編輯</button>
      {showEditor && <RichTextEditor />}

      <button onClick={() => setShowVideo(true)}>看影片</button>
      {showVideo && <VideoModal onClose={() => setShowVideo(false)} />}

      {/* HeavyChart 總是渲染,但直到這個組件出現在視口才下載 */}
      <HeavyChart data={chartData} />
    </div>
  );
}

3. Script 最佳化載入

tsx
import Script from 'next/script';

// strategy 決定 script 的載入時機:

// beforeInteractive — 頁面變成 interactive 之前載入(最高優先)
// 適合:Polyfills、關鍵的第三方 SDK
// 注意:會阻塞頁面,謹慎使用
<Script src="/polyfills.js" strategy="beforeInteractive" />

// afterInteractive(預設)— 頁面變成 interactive 後立刻載入
// 適合:Tag Manager、Analytics
<Script src="https://www.googletagmanager.com/gtag/js" strategy="afterInteractive" />

// lazyOnload — 瀏覽器空閒時才載入(最低優先)
// 適合:聊天客服、社群分享按鈕、廣告 SDK
<Script src="//cdn.example.com/chat-widget.js" strategy="lazyOnload" />

// worker — 在 Web Worker 中執行(不阻塞主線程)
// 需要安裝 @builder.io/partytown
<Script src="/heavy-analytics.js" strategy="worker" />

4. Bundle 大小分析

bash
# 安裝 bundle analyzer
npm install @next/bundle-analyzer

# next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer';

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
});

export default withBundleAnalyzer(nextConfig);

# 執行分析(會在瀏覽器開啟互動式分析圖)
ANALYZE=true next build

# 常見的 bundle 優化機會:
# - lodash:改用 lodash-es 或直接用原生 JS 方法
# - moment.js:改用 date-fns(按需引入)或 Day.js
# - @mui/icons-material:改用具名引入而非整包引入

高效能清單

  • LCP 元素加 priority 屬性
  • 所有 Image 設定 width + height
  • 字型使用 next/font/google
  • 大型組件使用 dynamic import
  • 第三方 Script 使用適當的 strategy
  • 動畫只用 transform 和 opacity

常見錯誤

  • 所有圖片都加 priority(失去 lazy loading 效益)
  • 使用原生 img 標籤載入大圖
  • 把整個 lodash 引入只用一個函式
  • 在主線程做昂貴的同步計算
  • 動畫使用 margin/top/left
  • 圖片沒有設定 sizes(讓瀏覽器下載過大的圖片)

Section 6:用 Lighthouse 找出問題

Lighthouse 是 Google 提供的開源自動化網頁品質工具,內建在 Chrome DevTools 中。它從 Performance、Accessibility、Best Practices、SEO 四個維度評分,並提供具體的改善建議,是效能優化的最佳起點。

使用 Lighthouse

bash
# 方法 1:Chrome DevTools
# 打開 DevTools → Lighthouse 分頁 → 選擇 Mobile / Desktop → Generate report

# 方法 2:CLI(適合 CI/CD 整合)
npm install -g lighthouse

# 跑分並產生 HTML 報告
lighthouse https://yoursite.com \
  --output html \
  --output-path ./lighthouse-report.html \
  --view  # 自動在瀏覽器開啟報告

# 只跑特定類別(節省時間)
lighthouse https://yoursite.com \
  --only-categories=performance,seo \
  --output json \
  --output-path ./report.json

# 方法 3:PageSpeed Insights(Google 線上工具)
# https://pagespeed.web.dev/
# 用真實的 Lighthouse 跑分 + 提供真實用戶的 CrUX 資料

Lighthouse 分數解讀

分數狀態說明
90 — 100🟢優秀達到業界最佳實踐,SEO 有加分效果
50 — 89🟡需要改善有明顯的效能問題,建議優先處理分數較低的類別
0 — 49🔴嚴重的效能問題,可能直接影響 SEO 排名和用戶體驗

最常見的 Lighthouse 建議

1. Serve images in next-gen formats

可節省 30-50% 圖片大小

建議將 JPEG/PNG 轉換為 WebP 或 AVIF 格式。WebP 比 JPEG 小 25-35%,AVIF 更小但相容性稍差。使用 next/image 會自動轉換為 WebP/AVIF,完全解決這個問題。

2. Eliminate render-blocking resources

可改善 200-500ms 的 LCP

某些 JS 和 CSS 在載入完成前會阻止頁面渲染。對 JS 加 defer/async,對不重要的 CSS 動態載入。Next.js 的 Script 組件幫你管理這個問題。

3. Reduce unused JavaScript

可節省 50-80% 的初始 JS 大小

頁面載入了大量從未被執行的 JS。解決方法是程式碼分割(Dynamic Import)和 Tree Shaking。用 Bundle Analyzer 找出最大的 chunk。

4. Properly size images

可節省 90% 以上的圖片傳輸量

圖片的實際像素遠大於顯示尺寸。例如上傳了 4000x3000px 的圖片,但只顯示 400x300px,浪費了 100 倍的資料量。使用 next/image 的 sizes 屬性解決。

5. Enable text compression

JS/CSS 壓縮後通常縮小 70%

HTML、JS、CSS 等文字資源沒有壓縮。啟用 gzip 或 brotli 壓縮可以大幅減少傳輸大小。Vercel 和大多數現代主機自動啟用。

在 CI/CD 中整合 Lighthouse

bash
# package.json — 加入效能測試 script
# "scripts": {
#   "perf:check": "lighthouse-ci autorun"
# }

# 安裝 Lighthouse CI
npm install -g @lhci/cli

# .lighthouserc.js — 設定效能門檻
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000', 'http://localhost:3000/blog'],
      numberOfRuns: 3,  // 跑 3 次取平均,減少波動
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],  // 效能 >= 90        'categories:seo': ['error', { minScore: 0.9 }],           // SEO >= 90        'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['warn', { maxNumericValue: 300 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',  // 上傳報告到臨時儲存(免費)
    },
  },
};

# 在 GitHub Actions 中使用:
# - name: Run Lighthouse CI
#   run: |
#     npm run build
#     npm start &
#     npx @lhci/cli autorun

效能優化的優先順序建議

效能優化是一個持續的過程,不要試圖一次解決所有問題。建議的優先順序:

  1. 先用 Lighthouse 找出最嚴重的問題(分數最低的建議)
  2. 修復 LCP(圖片優化、priority、字型)— 影響最大且通常最容易
  3. 修復 CLS(設定圖片尺寸、保留廣告空間)— 避免用戶點錯
  4. 改善 INP(程式碼分割、startTransition)— 需要更深入的分析
  5. 持續監控:在 CI/CD 設定效能門檻,防止退步
Core Web Vitals
LCP
INP
CLS
Lighthouse
Next.js
Web Performance