網站載入慢怎麼解決?先用 TTFB 定位瓶頸,再決定 CDN、Cache-Control 與 TTL 怎麼配,附壓縮、圖片格式與 HTTP/2 的真實收益和測量方法。
「網站載入慢」是最常見、也最容易被誤診的問題。慢可能出在 DNS 解析、TTFB、圖片體積、JavaScript 執行,也可能只是快取頭沒配對。這篇按真實診斷順序展開:先用命令定位瓶頸,再決定要不要上 CDN、快取設多久、圖片怎麼壓——而不是先買 CDN 再猜哪裡慢。
判斷順序:先看 TTFB,再看傳輸體積,最後看渲染阻塞。CDN 只能解決前兩項裡的一部分,第三項(JS 執行、重排)必須靠程式碼和構建側最佳化。
先定位瓶頸:載入慢到底慢在哪一段
把一次頁面載入拆成四段,問題通常就現形了:連線建立(DNS + TCP + TLS)、等待首位元組(TTFB)、傳輸(下載 HTML / CSS / JS / 圖片)、解析與渲染(JS 執行、佈局、繪製)。四段的最佳化手段完全不同,用錯工具等於白花錢。
- TTFB > 600ms:瓶頸在後端或區域。優先考慮 CDN + 換更近的源站區域
- TTFB < 200ms 但首屏慢:瓶頸在資源體積與阻塞,先壓圖片、合併請求
- 第二次訪問快、第一次慢:快取已經生效,重點最佳化首訪(預熱、關鍵 CSS 內聯)
定位只需一條命令,四個數字就能分清是誰的問題:
curl -s -o /dev/null -w "dns:%{time_namelookup}s tcp:%{time_connect}s ttfb:%{time_starttransfer}s total:%{time_total}s\n" https://example.com/
time_starttransfer 就是 TTFB。再加 -H 'Cache-Control: no-cache' 重測一次:如果 TTFB 明顯升高,說明之前命中的是 CDN 快取而不是源站的真實響應速度。
CDN 實際做了什麼:邊緣節點、反向代理與快取命中
CDN 本質是放在使用者旁邊的一層反向代理加快取。它把可快取內容快取在各主要城市、各大運營商出口的邊緣節點(PoP)上,使用者就近取資料,而不是每次都回源站。節點覆蓋、計費口徑與回源策略怎麼比,見 CDN 加速。
- 延遲降低:使用者到邊緣節點通常 5–30ms,替代掉回源的 100–200ms 跨境往返
- 快取命中:命中時完全不回源,實務中靜態資源的 CDN 命中率普遍在 80%–95%
- 源站減壓:頻寬與連線數壓力轉移到 CDN,源站只需保留兜底頻寬
- 協議最佳化:多數 CDN 預設支援 HTTP/2、HTTP/3 與 TLS 1.3,可統一開啟
怎麼確認是否命中:看響應頭。Cloudflare 是 cf-cache-status: HIT,AWS CloudFront 是 x-cache: Hit from cloudfront,Fastly 是 X-Cache: HIT,阿里雲 DCDN / 騰訊雲 CDN 常見為 X-Cache。欄位名各家不同,先 curl -sI https://你的域名/ 看一眼再判斷。
首次訪問未命中時,CDN 可能比直連源站還慢一點,因為多了一跳邊緣節點。所以規則要配對:靜態資源長快取,HTML 短快取。HTML 被長期快取才是 CDN 上線後最常見的翻車點。
TTFB 決定首屏上限,也直接決定轉化
TTFB(Time To First Byte)是瀏覽器發出請求到收到第一個位元組的時間。它決定了 HTML 什麼時候開始下載,也基本決定了 LCP 的下限——實測中 TTFB 常佔 LCP 的一半以上。Google 的參考線是 TTFB ≤ 800ms 為良好,Core Web Vitals 要求 LCP ≤ 2.5s,超出後留給你渲染的時間會被直接吃掉。
更現實的問題是營收。Google 與 SOASTA 的大型零售效能研究裡,移動端頁面速度每下降 1%,銷售額下降約 1%;在人為提速的 A/A 實驗中,首屏快 20% 對應轉化率提升約 9%。這類數字來自特定樣本,別當成自己的預期值,但方向和量級足夠說明:TTFB 是營收項,不是技術潔癖。
可落地的目標與手段:
- CDN 命中場景,把 TTFB 穩定控制在 200ms 以內
- 源站區域選在使用者附近,跨境往返每省 100ms,使用者感知就是「快了一檔」
- 用頁面級快取或反向代理快取,繞開重複的資料庫查詢
- 把首屏不需要的模組(推薦位、評論、第三方指令碼)改為非同步載入
判斷是否真的變快了:CDN 上線後 TTFB 從 150ms 掉到 30ms,但總時長沒變,說明瓶頸已經轉移到傳輸體積或渲染——這時繼續調快取是浪費時間,應該轉向圖片與 JS。
Cache-Control:把快取策略寫進響應頭
瀏覽器和 CDN 都只認響應頭。快取策略不寫在 Cache-Control 裡,預設行為就是「每次重新驗證」,等於沒開快取。下面四組是最常用的寫法。
帶內容 hash 的 JS / CSS
Cache-Control: public, max-age=31536000, immutable
HTML 頁面
Cache-Control: public, s-maxage=60, stale-while-revalidate=300
帶版本號的圖片
Cache-Control: public, max-age=2592000
登入後的頁面 / 個人資料
Cache-Control: private, no-store
max-age:瀏覽器快取秒數;s-maxage:共享快取(CDN)秒數,優先於 max-ageno-cache≠ 不快取:允許儲存,但每次必須回源校驗(可用 304,不傳 body)no-store:完全不寫快取,個人資料只該用這個immutable:重新整理頁面也不重新校驗,只對檔案 URL 帶 hash 時安全stale-while-revalidate:過期後先返回舊內容再後臺更新,HTML 強烈建議加
壓縮要生效,必須帶上 Vary: Accept-Encoding,否則 CDN 可能把 brotli 版本發給不支援的客戶端,或反過來發 gzip 版本。
TTL 怎麼定:靜態與動態內容的差別
TTL 的核心取捨只有兩個:更新延遲(內容多久能生效)與回源率(源站承受多少請求)。靜態資源把 TTL 拉長、更新交給檔名 hash;動態內容用短 TTL 換新鮮度。把靜態資源從應用伺服器搬到物件儲存再掛 CDN,是這一步最省事的做法——儲存分層與生命週期規則見 AWS S3 儲存分層。
| 內容型別 | 建議 Cache-Control | TTL 參考 | 說明 |
|---|---|---|---|
| 帶 hash 的 JS / CSS | public, max-age=31536000, immutable | 1 年 | 改內容就換檔名,可放心長快取 |
| 帶版本號的圖片 / 字型 | public, max-age=2592000 | 30 天 | 換 URL 或改檔名觸發更新 |
| HTML 頁面(文章、首頁) | public, s-maxage=60, stale-while-revalidate=300 | 60 秒 – 5 分鐘 | 發版後基本立即可見 |
| GET 型公開 API(列表、行情) | public, s-maxage=5-30 | 5–30 秒 | 按業務可接受的陳舊度定 |
| 帶鑑權的 API / 購物車 / 訂單 | private, no-store | 不快取 | 任何公共快取命中都是資料洩露 |
| 已登入頁面(HTML) | private, no-cache | 每次校驗 | 避免 A 使用者看到 B 使用者內容 |
| 第三方指令碼與字型 | 按供應商建議 | — | 優先本地託管,別把核心渲染壓在第三方上 |
動態內容從 5–30 秒起步,而不是直接 0:先觀察業務真實更新頻率,如果某介面一天只改幾次,TTL 放到 60 秒也沒問題,回源率能降一個量級。
瀏覽器快取、CDN 快取與源站快取的分工
一次請求會依次經過三層,能在上一層解決就不要往下傳。三層各自獨立配置,失效邏輯也不同。
- 瀏覽器快取:受
max-age控制,二次訪問零網路請求(DevTools 裡顯示 memory cache / disk cache) - CDN 快取:受
s-maxage控制,命中後不回源,是節省源站頻寬的主力 - 源站快取(Nginx / 應用層頁面快取):CDN 未命中時的最後一道防線,直接決定 TTFB
當 CDN 不快取或需要重新整理時,瀏覽器與源站之間靠協商快取:ETag(或 Last-Modified)配合請求頭 If-None-Match,內容沒變就回 304 Not Modified,不傳 body。協商快取省的是頻寬和時間,不會減少請求次數。
別把 HTML 設成長期快取。 頁面結構變了但使用者拿到的是舊 HTML,是比慢更嚴重的問題——發版後功能異常、連結 404。HTML 的 TTL 應該短到能被業務接受。
壓縮與圖片:省下來的都是傳輸時間
同等內容下,傳輸時間正比於位元組數。10Mbps 頻寬傳 1MB 需要約 0.8 秒,傳 300KB 只需要約 0.25 秒——這不是理論值,直接乘進首屏時間。
- 文字壓縮:HTML / CSS / JS 開 gzip 通常減少 60%–80%;brotli 在同等質量下再比 gzip 小約 15%–25%
- 圖片格式:WebP 相對 JPEG 常見節省 25%–34%,AVIF 相對 JPEG 常見節省約 50%
- 按螢幕發圖:用
srcset+sizes給 2 倍屏使用者發 2 倍圖,避免所有裝置都下載大圖 - 延遲載入:首屏以下的圖片加
loading="lazy",首屏主圖不要加 - 預留尺寸:圖片寫明
width/height,把 CLS 壓到 0.1 以內
LCP 圖要特殊對待:加 fetchpriority="high" 或 <link rel="preload" as="image"> 提前發起請求,並且排除 lazy load。它是首屏最大的資源,搶不到請求槽位,壓縮收益再高也沒用。
多數 CDN 支援按 Accept 請求頭自動轉換 WebP / AVIF,並在邊緣完成 brotli 壓縮。啟用前確認兩件事:廠商是否支援,以及轉換後是否仍走同一套快取鍵(否則一個 URL 會快取兩份格式,命中率白白下降)。
HTTP/2 與 HTTP/3:連線複用到底值多少
一個包含 40 個靜態資源的頁面,在 HTTP/1.1 下通常要開 6 個連線、經歷多次 TCP 與 TLS 握手。HTTP/2 用單連線多路複用加 HPACK 頭部壓縮,一次握手就能併發請求,實測一般能省 100–300ms 的首屏時間。
- CDN 與源站都要開 H2。只在 CDN 開,邊緣到源站那段仍走 HTTP/1.1,收益減半
- 不要再做「合併成一個大檔案」「跨域分片域名」——這是 HTTP/1.1 時代的解法,在 H2 下反而拖慢解析
- HTTP/3 基於 UDP / QUIC:支援 0-RTT 恢復與更好的丟包恢復,行動網路切換時體驗提升明顯
HTTP/3 要灰度再上。 部分企業防火牆、老舊運營商網路與中間裝置會阻斷 UDP。可用 curl --http3 -s -o /dev/null -w "%{http_version} %{time_starttransfer}s\n" https://example.com/ 驗證協商是否成功,失敗時應自動回落到 HTTP/2。
內容變了,快取怎麼失效
長快取帶來長 TTL,也就帶來「改了內容使用者看不到」的問題。按可靠性排序,三種方案優先順序不同。
- 版本化 URL(首選):檔名或查詢串帶內容 hash,如
app.4f3a2b.js。新內容天然是新 URL,不需要任何重新整理動作,CDN 命中率不受影響 - 按 URL / 目錄重新整理:內容必須保持同一 URL 時才用。釋出流水線裡發一次重新整理請求,通常幾秒到幾十秒內全網生效
- 預熱:釋出後主動回源拉取新版本(
curl預熱介面或控制檯預熱),避免冷啟動時大量使用者同時穿透到源站
三種方案的生效時間:版本化 URL 首次訪問即生效(無需任何操作);單 URL 重新整理通常幾秒到幾十秒內全網生效,介面級重新整理介面限速的差異較大;預熱則是主動提前拉取,成本最低但需要你在流水線裡多寫一步。
注意重新整理本身有成本:全量重新整理會造成回源風暴,大流量站點應在低峰期執行,或配置重新整理限速與自動預熱。重新整理後立即用 curl -sI 複驗一次,確認拿到的是新版本再繼續釋出後續變更。
怎麼測量:Lighthouse、WebPageTest 與 TTFB 基線
最佳化必須先有基線。沒有改前改後的對比,任何效能結論都是猜測。三個工具各管一件事:
- Lighthouse(Chrome DevTools → Lighthouse,或
npx lighthouse https://example.com --only-categories=performance):給 LCP、CLS、TBT 與「減少初始伺服器響應時間」審計項 - WebPageTest:多地節點對比 TTFB、First Contentful Paint,並區分 First View 與 Repeat View,直接看出 CDN 是否生效
- DevTools Network 瀑布圖:看每條請求的 Size 列、
Status 200 / 304、Priority,以及memory cache/disk cache命中標記
| 指標 | 良好 | 偏差 | 工具 |
|---|---|---|---|
| TTFB | ≤ 0.8s(CDN 命中建議 ≤ 0.2s) | > 1.8s | WebPageTest / curl |
| LCP | ≤ 2.5s | > 4.0s | Lighthouse / CrUX |
| INP | ≤ 200ms | > 500ms | Lighthouse / 真實使用者監控 |
| CLS | ≤ 0.1 | > 0.25 | Lighthouse |
實驗室資料不等於真實體驗。 辦公室網路通常又快又穩,測出來一切都好。條件允許時接入 RUM(真實使用者監控)或至少用 WebPageTest 多地節點複核,避免最佳化只對測速工具有效。
一週可執行的最佳化清單
按這個順序做,一週內就能看到 TTFB 和首屏的明確變化。每一步都有可驗證的數字。
- 第 1 天 · 建基線:用上面的
curl命令記錄 DNS / TCP / TTFB / 總時長,再跑一次 Lighthouse,把四項資料存檔 - 第 2 天 · 分層診斷:找出 TTFB > 600ms 的介面,和體積最大的 5 個資源,其餘先不動
- 第 3 天 · 快取頭:給帶 hash 的 JS / CSS 設一年
immutable,HTML 設s-maxage=60+stale-while-revalidate,確認Vary: Accept-Encoding - 第 4 天 · 上 CDN:先只接管靜態資源,驗證
X-Cache命中頭與 TTFB 是否降到 200ms 以內,再考慮接入 HTML - 第 5 天 · 壓縮與圖片:開啟 brotli,WebP / AVIF 自動轉換,首屏圖加
fetchpriority="high"並去掉 lazy load - 第 6–7 天 · 協議與複核:確認 CDN 與源站都開了 HTTP/2,灰度驗證 HTTP/3,重跑 Lighthouse 與 WebPageTest 對比基線
下一步:先只做前三步(建基線 + 改快取頭),這一步不需要採購新資源,卻通常能砍掉相當一部分重複請求。如果 TTFB 仍然偏高,再去評估 CDN 與更近的源站區域——把上面 curl 命令測出的三個數字(TTFB / 總時長 / 是否命中快取)整理成一張表,拿去比方案會高效得多。