Notes
← All notes
·frontend·10 min

前端效能指標深入解析:INP、LCP、TTFB、SSR 與 ISR 的真實關係

從 INP 替換 FID 開始,拆解 LCP 四層架構、TTFB 完整路徑,再到 SSR/ISR/Static SPA 的取捨——搞懂每個指標背後在量什麼,才能針對問題下手。

#web-performance#core-web-vitals#INP#LCP#TTFB#SSR#ISR#Next.js

前陣子在深入研究 K-Line 專案的效能優化時,碰到一個困惑:Lighthouse 給不出 INP 的分數,而 SSR 的 TTFB 明顯比 Static SPA 長,但有人說 SSR 的 LCP 反而更好。這兩件事乍看矛盾,但理解之後其實很合理。這篇文章從這兩個困惑出發,把幾個彼此有關係的指標和架構選擇梳理清楚。


FID 退役,INP 接班——為什麼這個替換很重要

Google 在 2024 年 3 月把 INP(Interaction to Next Paint) 正式納入 Core Web Vitals,同時讓 FID(First Input Delay)退役。

FID 量的是:使用者第一次互動(點擊、鍵盤輸入、tap)到瀏覽器開始處理這個事件之間的延遲。問題在於「第一次」——它只量一個點,整個瀏覽過程中後續所有互動的反應速度完全不納入評估。

INP 的設計不同。它量的是整個 session 裡所有互動的 next paint 延遲,取最壞情況(排除少數極端值後的最高值)。也就是說,哪怕你的 FID 很快,只要使用者在操作途中踩到一個慢的互動,INP 就會揭露它。

指標量什麼問題
FID第一次互動的事件處理延遲只量一次,看不到後續互動的問題
INP所有互動 → 畫面更新的端對端延遲更貼近使用者的實際感受

INP 的閾值:Good ≤ 200ms,Poor > 500ms。超過 200ms,使用者開始感覺卡頓。

INP 沒辦法用 Lighthouse 測

這是第一個重要的認知差距:Lighthouse 跑出來的報告沒有 INP 分數

原因是 INP 需要真實的使用者互動才能觸發——它必須在真正有人在點擊、滾動、輸入的環境下測量。Lighthouse 是 synthetic 測試,它模擬頁面載入過程,但不模擬使用者互動序列。

要取得 INP 資料,有兩條路:

  1. Google Analytics 4 + web-vitals library:在 production 環境收集真實使用者的 INP,在 GA4 裡查看 Core Web Vitals 報告。
  2. Chrome DevTools Performance panel:手動操作頁面,在 Performance 錄製中觀察 interaction 事件的延遲。

LCP 不只是「圖片載慢了」——四層拆解

LCP(Largest Contentful Paint) 是頁面上最大的可見元素完成渲染的時間點。但「LCP 太慢」可以來自四個完全不同的地方:

[navigate] → TTFB → [blocking] → [element load] → [render] = LCP
定義修法方向
TTFB請求送出 → 收到第一個 byteCDN、server latency
BlockingHTML 收到 → LCP element 開始 fetch移除 render-blocking scripts / fonts
Element loadLCP element fetch 開始 → 完成壓縮圖片、加 preload
RenderFetch 完成 → 畫在螢幕上JS 阻塞 main thread(INP 的領域)

這四層要分開看。如果 TTFB 本身就長(比如 SSR 沒有 cache),就算後三層都很快,LCP 還是起跑晚。如果 element load 很慢(大張 PNG 沒有壓縮),加再多 preload 也只是「更早開始等」,不是真正快了。

混在一起看只會把不同問題的解法搞混。舉個例子:上一篇 K-Line 優化文章裡,LCP element(hero image)從 3.1s 降到 0.8s,主要靠的是 Element load 層的優化(PNG → WebP,-59% 大小),加上 Blocking 層的優化(self-hosted fonts 移除 render-blocking stylesheet)。這兩層改了,分數才有感。


TTFB 的完整路徑:從 DNS 到第一個 byte

TTFB(Time to First Byte) 量的不只是「server 回應速度」,它包含整個網路旅程:

DNS lookup → TCP handshake → TLS handshake → server processing → first byte

每一段都可能是瓶頸。

Static SPA on CDN vs SSR without cache

最容易說明 TTFB 差異的對比就是這兩種架構:

Static SPA on CDN:

  • 使用者請求 / → CDN edge node 直接回傳 pre-built 的 index.html
  • Server 不需要執行任何程式碼,純粹回傳靜態檔案
  • TTFB 非常短,通常 < 100ms

SSR without cache:

  • 使用者請求 / → server 必須先執行 React render(或 Next.js 的 getServerSideProps)產生 HTML
  • HTML 產生完才開始回傳,第一個 byte 要等 server 執行完
  • TTFB 可能到數百 ms 甚至更長,視 server 的 loading 和資料查詢速度

TTFB 的 Good/Poor 門檻:Good ≤ 800ms,Poor > 1800ms。


SSR vs Static SPA:TTFB 短不代表使用者看到內容快

這是最容易誤解的部分。TTFB 短,不代表 LCP 就好

Static SPA 的陷阱:空殼 + JS hydration

Static SPA 的 index.html 大概長這樣:

<body>
  <div id="root"></div>   <!-- 空的 -->
  <script src="/main.js"></script>
</body>

TTFB 極短,但 HTML 本身沒有內容——是個空殼。瀏覽器收到之後,還要:

  1. 下載 JS bundle(可能幾百 KB)
  2. 執行 JS,React render 出 DOM
  3. 才有 LCP element 可以渲染

這個 JS 下載 + 執行的時間,就是 LCP 的真正成本。TTFB 短,但 LCP 可能很慢。

SSR 的反直覺優勢

SSR 的 index.html 直接包含 render 好的 HTML:

<body>
  <div id="root">
    <h1>K-Line Prediction</h1>
    <img src="/hero-shot.webp" />  <!-- LCP element,已經在 HTML 裡 -->
    ...
  </div>
</body>

TTFB 比 Static SPA 長(server 要 render),但 HTML 送達後,瀏覽器馬上可以看到 LCP element,不需要等 JS 執行。結果:

架構TTFB使用者看到內容(LCP)
Static SPA短(CDN)慢(等 JS hydration)
SSR(無 cache)長(server render)快(HTML 已有內容)
SSR(有 cache / ISR)

所以「SSR 比較慢」這個說法是不完整的——TTFB 確實比較長,但 LCP 可以更好,使用者實際感受到的「頁面出現了」反而更快。


ISR:靜態的速度 + 動態的資料

ISR(Incremental Static Regeneration) 是 Next.js 的功能,試圖結合 Static SPA 的短 TTFB 和 SSR 的新鮮資料。

stale-while-revalidate 的運作方式

ISR 的核心概念是 stale-while-revalidate

  1. 第一次有人請求某個頁面 → 靜態生成 HTML,存在 CDN
  2. 後續請求直接從 CDN 回傳靜態頁面(TTFB 極短)
  3. 超過 revalidate 設定的秒數後,下一個請求觸發背景重新生成
  4. 重新生成完成後,後續請求才拿到新版本
// Next.js App Router
export const revalidate = 86400; // 每 24 小時重新生成一次

按需再生成,不是 cron

這個設計細節很重要:ISR 的再生成是由請求觸發的,不是定時排程。

沒有流量 → 不觸發再生成 → 不消耗資源。對於低流量頁面或長尾頁面,這個設計非常省成本。相比之下,如果是 cron job 定時重新 build 整站,就算沒有人在看某個頁面,資源還是在跑。

revalidate 值怎麼選

revalidate 值應該對應資料的更新頻率。以 K-Line 日 K 資料為例:

  • 日 K 每天更新一次(台灣時間收盤後)
  • revalidate = 86400(24 小時)合理,不會有人看到超過一天的舊資料
  • 如果資料每小時更新,就設 3600

ISR 的死角

有一個無法避免的 tradeoff:每個 revalidate 週期過後的第一個訪客,一定會看到舊資料

流程:

  1. revalidate 時間到了
  2. 第一個訪客請求頁面 → 觸發背景再生成,但這個訪客拿到的是舊版本
  3. 再生成完成後 → 第二個訪客才拿到新版本

這個 staleness 對大多數內容型頁面(部落格、產品介紹)是可以接受的。但對即時性要求高的資料,ISR 不夠用。

什麼情況 ISR 不適合

股票即時價格、剛下的訂單狀態、需要立刻反映的使用者資料——這些都不適合 ISR。ISR 的 staleness 是設計上的 tradeoff,不是 bug 可以修掉的問題。


即時資料的正確姿勢

對於需要即時資料的頁面,正確的 pattern 不是「用 SSR 每次都重新 render」,而是分層處理:

SSR 或 ISR 管頁面結構(HTML、SEO 內容、靜態部分)→ WebSocket 或 polling 在 client 管即時價格

// 頁面結構由 ISR 處理(快 TTFB)
export const revalidate = 3600;

// 即時價格在 client 端另外處理
function StockPrice({ ticker }: { ticker: string }) {
  const [price, setPrice] = useState<number | null>(null);

  useEffect(() => {
    const ws = new WebSocket(`wss://api.example.com/price/${ticker}`);
    ws.onmessage = (e) => setPrice(JSON.parse(e.data).price);
    return () => ws.close();
  }, [ticker]);

  return <span>{price ?? "—"}</span>;
}

K-Line 預測結果為什麼 client-side fetch 就夠

K-Line 的預測結果是 user-triggered:使用者點擊「查詢」,backend 才跑相似度計算,回傳結果。這個結果:

  1. 不需要在頁面載入時就存在(不影響 LCP)
  2. 不需要 SEO 索引
  3. 每次查詢的結果因使用者輸入而異

這三個條件都指向同一個結論:client-side fetch 就夠了,不需要 SSR 或 ISR。強行用 SSR 反而增加 server loading,而且使用者還是要等查詢完成才有結果——SSR 的優勢完全用不上。


小結

指標 / 架構量什麼重點
INP所有互動 → next paint 端對端Lighthouse 測不到,需要 real user monitoring
LCP最大可見元素完成渲染四層各有不同修法,不要混在一起看
TTFB第一個 byte 到達時間CDN static = 短;SSR no-cache = 長
Static SPATTFB 短,LCP 慢空殼 + JS hydration 是真正成本
SSRTTFB 長,LCP 可能更好HTML 送達時已有內容,不需等 JS
ISRTTFB 短 + 資料定期刷新stale-while-revalidate;即時資料不適用

Web performance 的指標很多,容易讓人覺得「把每個分數都提高」就是目標。但更重要的是搞清楚每個指標在量什麼、它反映的是哪一層的問題。對症下藥,才不會把時間花在不影響使用者體驗的地方。