TTFB 與 FCP:伺服器回應與首次繪製的優化
這兩個是 輔助指標,不列入 三大核心指標 的評分,但它們決定了核心指標的上限。
| 指標 | 量什麼 | 建議門檻 |
|---|---|---|
| TTFB | 請求發出到收到第一個位元組 | ≤ 800 毫秒 |
| FCP | 畫面第一次出現任何內容 | ≤ 1.8 秒 |
TTFB:伺服器多快開始回話
TTFB(Time to First Byte,第一個位元組時間)是請求送出到收到回應 第一個位元組 的時間,量的是伺服器多快開始回話,還沒談到畫面上出現什麼。
TTFB 的六段組成
TTFB 不是單一動作,而是六件事的總和:
| 階段 | 做什麼 | 慢的原因 |
|---|---|---|
| 1. 轉址 | 若有轉址,先跑一輪 | 轉址鏈 |
| 2. DNS 查詢 | 網域轉成 IP | DNS 供應商慢、沒有預先解析 |
| 3. 建立連線 | TCP 三次握手 | 實體距離遠 |
| 4. TLS 交握 | HTTPS 的加密協商 | 憑證鏈太長、不支援連線恢復 |
| 5. 伺服器處理 | 查資料、組裝回應 | 通常是主因 |
| 6. 回應傳輸 | 第一個位元組送達 | 頻寬不足 |
第 1 段最容易被忽略
http 轉 https、非 www 轉 www,每一次轉址都是完整的一輪往返。兩次轉址串起來,TTFB 直接多幾百毫秒。
修法是 一跳到底:讓所有變體直接轉到最終網址,不要串接。做法見 轉址與 HTTP 狀態碼 。
怎麼量測各段
// 從導覽計時取出各階段的耗時
const [nav] = performance.getEntriesByType('navigation');
console.log({
轉址: nav.redirectEnd - nav.redirectStart,
DNS: nav.domainLookupEnd - nav.domainLookupStart,
連線: nav.connectEnd - nav.connectStart,
TLS: nav.connectEnd - nav.secureConnectionStart,
伺服器處理: nav.responseStart - nav.requestStart,
TTFB: nav.responseStart,
});伺服器處理那一段最值得先看
上面的 responseStart - requestStart 就是伺服器真正花的時間。如果它佔了 TTFB 的大半,優化方向就在後端而不是網路。
DevTools 的 Network 面板點開任一請求的 Timing 分頁也看得到同樣的拆解。
縮短 TTFB 的手法
改用靜態產生
| 渲染模式 | 第 5 段的耗時 |
|---|---|
| 靜態產生(SSG) | 幾乎為零 |
| 增量靜態再生(ISR) | 命中快取時幾乎為零 |
| 用戶端渲染(CSR) | 幾乎為零(但內容要等程式碼) |
| 伺服器端渲染(SSR) | 要查資料與組裝 |
SSR 的 TTFB 本來就比較慢
這不是設定錯誤。伺服器要查資料庫、跑模板、組裝 HTML 才能回傳第一個位元組。
SSR 買到的是「首屏內容早點出現」與「爬蟲讀得到」,代價就是 TTFB。取捨見 渲染模式怎麼選 。
用 CDN
CDN 把資源放在離使用者近的節點,縮短第 3、4、6 段。
| 資源類型 | CDN 的效果 |
|---|---|
| 靜態資源(圖片、CSS、JS) | 幾乎一定有效 |
| 靜態 HTML | 有效 |
| 動態頁面 | 要看有沒有邊緣快取 |
單純代理不會加快伺服器處理
如果 CDN 只是把請求轉回源伺服器,第 5 段完全沒改善,只縮短了網路距離。
動態頁面要靠 邊緣快取:讓 CDN 節點自己保存一份回應,命中時不必回源。
設定快取標頭
# 帶版本號或雜湊的靜態資源:可以快取很久
Cache-Control: public, max-age=31536000, immutable
# HTML:不要長期快取,但允許先用舊的再背景更新
Cache-Control: public, max-age=0, must-revalidate
# 或用 stale-while-revalidate 讓重複造訪幾乎沒有等待
Cache-Control: public, max-age=60, stale-while-revalidate=86400| 指令 | 作用 |
|---|---|
max-age | 快取多久(秒) |
immutable | 保證內容不會變,瀏覽器不必重新驗證 |
must-revalidate | 過期後必須向伺服器確認 |
stale-while-revalidate | 過期後先用舊的,背景更新 |
immutable 要搭配檔名雜湊
app.a1b2c3.js 這種帶雜湊的檔名,內容變了檔名也變,所以可以放心設一年的快取。
沒有雜湊的 app.js 設了長快取就很難更新,使用者可能一年都拿到舊版。建置工具通常會自動處理檔名雜湊。
減少轉址
# 三跳:http → https → www → 最終
# 改成一跳到底
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri;
}後端的最佳化
第 5 段的優化屬於後端範圍,但工程師該知道方向:
| 手法 | 說明 |
|---|---|
| 資料庫查詢加索引 | 最常見的瓶頸 |
| 避免 N+1 查詢 | 一次取回關聯資料 |
| 加一層應用層快取 | Redis 之類的記憶體快取 |
| 非必要的資料延後取 | 首屏用不到的資料改成之後再要 |
| 串流回應 | 先送檔頭,內容邊算邊送 |
串流回應能顯著改善 TTFB
支援串流的框架可以 先把 <head> 送出去(讓瀏覽器提早開始下載 CSS 與字型),內容部分邊算邊送。
這樣 TTFB 大幅縮短,而總時間不變,但使用者感受到的速度明顯改善。
FCP:畫面第一次出現內容
FCP(First Contentful Paint,首次內容繪製)是畫面上 第一次出現任何內容 的時間,一段文字、一個圖示、一個色塊都算。
| FCP | LCP | |
|---|---|---|
| 量什麼 | 第一次出現 任何 內容 | 主要 內容繪製完成 |
| 建議門檻 | ≤ 1.8 秒 | ≤ 2.5 秒 |
| 算核心指標 | 否 | 是 |
FCP 是 LCP 的下限
FCP 之前不可能有 LCP,什麼都還沒畫,怎麼會有最大元素?
所以改善 FCP 通常會 帶動 LCP。兩者的優化手法也高度重疊。
改善 FCP 的手法
| 手法 | 為什麼有效 |
|---|---|
| 縮短 TTFB | FCP 的起點就是 TTFB |
| 移除 阻塞渲染的資源 | CSS 沒解析完什麼都不會畫 |
| 首屏樣式內嵌 | 不必等外部 CSS |
字型加 font-display: swap | 文字不必等字型 |
用 preconnect | 提早建立第三方連線 |
<!-- 首屏樣式內嵌,其餘非同步 -->
<style>
/* 只放首屏需要的 */
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { min-height: 400px; }
</style>
<link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'">這個站的做法
這是一個 靜態產生 的文件站,所以 TTFB 的第 5 段幾乎為零,建置時就把 HTML 準備好了:
# 建置產出純靜態檔案
npm run build| 決定 | 對 TTFB 的影響 |
|---|---|
| 靜態產生而非 SSR | 伺服器處理時間趨近零 |
| 橫幅用 SVG(幾 KB) | 第 6 段的傳輸量小 |
| 不用網頁字型 | 少一輪字型請求 |
| 樣式由建置工具打包並加雜湊 | 可設長快取 |
檢查清單
| 項目 | 標準 |
|---|---|
| TTFB 在 800 毫秒內 | 目標 |
| FCP 在 1.8 秒內 | 目標 |
| 沒有轉址鏈(一跳到底) | 必備 |
| 靜態資源走 CDN | 建議 |
帶雜湊的資源設長快取加 immutable | 建議 |
| HTML 沒有設長快取 | 必備 |
| 阻塞渲染的資源已處理 | 建議 |
| 內容不常變的網站用靜態產生 | 建議 |
| 已用導覽計時拆解各段 | 建議 |
常見問題
TTFB 算核心指標嗎?
不算,它是輔助指標。但它是所有載入指標的起點,TTFB 有一秒,最大內容繪製 就不可能低於一秒。所以它雖然不被直接評分,卻決定了另一個指標的天花板。
合理的門檻是多少?
TTFB 建議在 800 毫秒內,FCP 建議在 1.8 秒內。這兩個都是輔助指標的參考值,不像三大核心指標有明確的評級門檻。
為什麼伺服器端渲染的 TTFB 反而變差?
因為伺服器要查資料、組裝 HTML 才能回傳第一個位元組,而純靜態檔案是直接送出。伺服器端渲染買到的是首屏內容早點出現與可被爬取,不是全面更快,取捨見 渲染模式怎麼選 。
FCP 和 LCP 差在哪?
FCP 是畫面第一次出現任何內容的時間,可能只是一段文字或一個色塊;LCP 是主要內容繪製完成的時間。前者是後者的下限,改善前者通常也會帶動後者。
CDN 一定能改善 TTFB 嗎?
對靜態資源幾乎一定有效,對動態頁面則要看有沒有做邊緣快取。單純把請求代理回源伺服器的設定不會加快伺服器的處理時間,只縮短了網路距離那一段。