題幹與適用情境
一個 Web 應用程式包含四類 GET 回應:
/index.html:URL 固定,每次發布都會更新內容,並引用目前版本的靜態資源;/assets/app.8f3c.js:檔名包含內容雜湊,內容改變時 URL 也會改變;/api/catalog:公開商品目錄,伺服器依照Accept-Language回傳語言版本,最多可接受約 60 秒延遲;/api/cart:登入使用者的個人化購物車,不能被 CDN 或其他使用者重用。
一次發布後,部分使用者仍使用舊 HTML,並請求部署時已刪除的舊 JS;商品目錄偶爾顯示錯誤語言;團隊還把 no-cache 當成「完全不儲存」,導致討論無法轉成可執行設定。請解釋瀏覽器私有快取、CDN 共享快取、新鮮度、重新驗證和快取鍵如何配合,並為四類回應設計回應標頭、發布順序和驗證方法。
60 秒是面試假設,不代表來源中的正式環境指標。基本範圍只討論 HTTP 快取;Service Worker Cache、應用程式記憶體快取和框架資料快取留到追問。這題適合前端、全端和 Web 效能職缺,核心能力是瀏覽器與 HTTP 平台行為,因此歸入 frontend。
面試官考察重點
第一個訊號是能否用四個問題推導快取策略:回應能否儲存、誰可以重用、多久仍算新鮮、過期後如何驗證。只背 Cache-Control 指令很容易把「儲存」與「直接重用」混在一起;好的回答會先定義資料邊界,再寫回應標頭。
第二個訊號是能否正確區分 no-cache 與 no-store。no-cache 允許儲存,但每次重用前都必須向來源站驗證;no-store 才表示快取不應儲存該回應。所有動態內容都使用 no-store,會失去條件式請求和部分瀏覽器能力,只有明確禁止進入 HTTP 快取時才應使用。
第三個訊號是能否說清楚 freshness 與 validation。回應仍新鮮時,快取可以直接重用;回應過期或被要求驗證時,用戶端可以帶上 If-None-Match 或 If-Modified-Since。資源未變更時,伺服器回傳 304 Not Modified,快取更新中繼資料並重用既有回應內容。
最後要看候選人是否理解發布流程與快取鍵。正確的靜態資源回應標頭也無法挽救「舊 HTML 指向已刪除檔案」的發布流程;缺少 Vary: Accept-Language 又可能讓共享快取把同一 URL 的中文表示用在英文請求。好的回答會同時涵蓋回應標頭、資源生命週期、語言變體與可重現驗證。
回答前需要釐清的問題
- 四類回應是否都經過 CDN?
s-maxage只控制共享快取,不影響瀏覽器私有快取。沒有 CDN 時,公開 API 的共享快取策略可以簡化。 - 商品目錄的語言來自 URL 還是請求標頭? 如果 URL 是
/api/catalog?locale=zh-CN,不同 URI 已形成不同快取鍵;如果同一 URL 依Accept-Language變化,就需要正確設定Vary。 - 購物車是否允許瀏覽器儲存後再驗證? 如果允許單一使用者的私有快取儲存,可用
private, no-cache;若資安或產品要求禁止任何 HTTP 快取儲存,就使用no-store。 - 舊雜湊資源可以保留多久? 如果發布後立刻刪除舊檔案,仍持有舊 HTML 的分頁、歷史導覽或回滾版本都會失敗。保留時間要涵蓋用戶端可能持有舊入口的期間與回滾期間。
- 目錄資料可以接受多舊? 「最多 60 秒」必須區分正常新鮮期,以及故障時是否允許額外使用舊回應;如果不能接受過期資料,就不能用
stale-while-revalidate或stale-if-error放寬重用。 - 是否有 Service Worker? Service Worker 可以攔截請求並從 Cache Storage 回傳內容,形成獨立於一般 HTTP 快取的決策層。若存在,必須一起檢查。
30 秒回答框架
「我會先對每個回應判斷能不能存、誰能重用、多久新鮮、過期後怎麼驗證。帶雜湊的 JS 用一年 max-age 加 immutable,並保證同一 URL 永不修改內容;HTML 用 no-cache 和 ETag,讓瀏覽器可以儲存,但每次正常導覽都驗證。公開目錄允許 CDN 快取 60 秒,可以用 s-maxage=60、ETag,並在同一 URL 依語言回傳時加 Vary: Accept-Language。購物車至少標成 private,允許瀏覽器儲存時搭配 no-cache,明確禁止儲存時才用 no-store。發布時先上傳新資源,再切換 HTML,並保留舊雜湊資源。最後驗證無快取首次請求、fresh 命中、304 驗證、語言變體和舊 HTML 重播。」
分步深入解答
第一步:用儲存、重用、新鮮度和驗證建立模型
HTTP 快取不是單一開關。一次請求至少要經過四個判斷:
- 能否儲存:
no-store禁止儲存;其他回應能否儲存還受方法、狀態碼、授權與明確快取指令影響。 - 誰能重用: 瀏覽器是私有快取,通常只服務一個使用者;CDN、代理伺服器是共享快取。
private禁止共享快取重用,public可以明確允許共享快取。 - 是否仍新鮮:
max-age=N定義私有與共享快取通常可直接重用的時間,s-maxage=N在共享快取中覆蓋max-age。回應目前的年齡也會受到Date和Age等資訊影響。 - 過期後怎麼辦: 快取可以用驗證器發出條件式請求。未變更時回傳 304 並重用既有回應內容,變更時回傳包含新內容的 200。
因此,「快取命中」也有兩種不同成本:fresh hit 不需要向上游確認;revalidated hit 仍有網路往返,但可避免重新傳輸完整內容。面試回答應明確說出是哪一種。
第二步:讓內容雜湊靜態資源長期快取
只要建置系統保證內容改變就產生新 URL,靜態資源可以回傳:
Cache-Control: public, max-age=31536000, immutable31536000 秒是一年。immutable 表示資源在新鮮期間不會改變,可避免部分重新載入情境產生無意義驗證。關鍵不在「一年」這個數字,而在兩個不變量:
- 同一個雜湊 URL 的位元組內容永遠不變;
- 新版本使用新 URL,並由新 HTML 引用。
如果伺服器在同一路徑覆蓋檔案,長期快取會讓用戶端長期使用舊內容。若發布系統刪除舊雜湊檔案,即使新版本設定正確,舊分頁、瀏覽器歷史、分階段發布節點或回滾版本仍可能請求舊檔案。穩健流程是先上傳新資源,確認可存取後再發布 HTML,並讓舊雜湊資源至少保留到舊入口與回滾期間結束。
第三步:讓固定 URL 的 HTML 每次重新驗證
入口 HTML 的 URL 無法像靜態資源一樣隨內容改變,因此可以回傳:
Cache-Control: no-cache
ETag: "index-v184"瀏覽器可以儲存 HTML,但正常重用前需要驗證。下一次請求會帶上:
If-None-Match: "index-v184"若內容未變,伺服器回傳 304;若已發布新入口,則回傳 200 與新的 ETag。ETag 是某個表示版本的識別碼,不要求一定是內容雜湊,但資源變更時必須變更。也可以同時提供 Last-Modified;當請求同時帶有 If-None-Match 與 If-Modified-Since 時,ETag 條件較精確,應依標準優先處理。
no-cache 不代表「不儲存」。如果產品要求 HTML 完全不進入 HTTP 快取,才考慮 no-store,但這會放棄 304 等效益。還要說明瀏覽器的前進/返回快取可能直接恢復頁面快照,不走一般 HTTP 驗證流程,所以排查「返回後看到舊頁面」時不能只看 Cache-Control。
第四步:分別控制公開 API 的瀏覽器與 CDN
假設商品目錄正常可容忍 60 秒延遲,並希望瀏覽器每次都向 CDN 確認、CDN 最多直接重用 60 秒,可以設定:
Cache-Control: public, max-age=0, s-maxage=60
ETag: "catalog-v927"
Vary: Accept-Language各指令的作用要分開理解:
max-age=0讓瀏覽器儲存後立即視為過期,後續請求進入驗證路徑;s-maxage=60讓共享快取可在 60 秒內直接重用;ETag讓 CDN 或瀏覽器在過期後用條件式請求減少傳輸;Vary: Accept-Language告訴快取把語言請求標頭納入選擇回應的快取鍵。
如果業務接受額外 30 秒舊資料換取較低的驗證延遲,可以追加 stale-while-revalidate=30。這個標準指令會影響所有支援它的快取,不只 CDN;如果只想調整 CDN,應使用所選 CDN 明確提供的專用設定。業務承諾是「任何情況都不能超過 60 秒」時,不應 增加這段 stale 期間。它是延遲與新鮮度之間的明確交換,不能因為看起來更快就預設加入。
語言最好在 URL 中明確表達,例如 /api/catalog?locale=zh-CN,如此記錄、預熱、失效和快取鍵都更可控;採用這種設計後,不再需要用 Accept-Language 區分同一 URL 的表示。不要輕易使用 Vary: User-Agent,取值過多會大幅降低共享快取重用率。
第五步:保護個人化購物車
購物車不能被共享快取提供給其他使用者。若允許瀏覽器儲存目前使用者的回應,並要求每次使用前確認最新狀態,可以回傳:
Cache-Control: private, no-cache
ETag: "cart-v19"private 限定只能由私有快取儲存;no-cache 要求重用前驗證。不要用 Vary: Cookie 嘗試隔離使用者,Cookie 取值基數極高,容易造成快取鍵與隱私風險;個人化回應應明確禁止共享快取。
如果資安要求是「回應不得寫入任何 HTTP 快取」,則改為:
Cache-Control: no-store不需要再疊加 private, no-cache, max-age=0。這些更嚴格或彼此重疊的指令不會讓策略更安全,只會讓意圖更難稽核。還要注意:替後續回應加上 no-store 不會自動清除先前已儲存且仍新鮮的副本;事故處理需要 CDN purge、版本化 URL、等待過期或瀏覽器端的專項清理方案。
第六步:把快取策略與發布協定綁在一起
本題的舊 JS 故障不能只靠修改回應標頭修復。發布協定應符合:
- 上傳所有新雜湊資源;
- 從正式環境邊緣節點驗證新資源可存取,且 MIME、壓縮和回應標頭正確;
- 發布引用新 URL 的 HTML;
- 保留舊雜湊資源,涵蓋舊分頁、分批發布和回滾期間;
- 發生回滾時恢復舊 HTML,而舊資源仍然存在;
- 只有確認沒有可達入口引用舊資源後才清理。
如果 HTML 由多個節點產生,ETag 也必須與表示一致。不同節點對相同內容產生不同 ETag,會增加無效 200;不同內容錯誤重用同一個強 ETag,則可能回傳錯誤 304。壓縮、語言等表示變化還要搭配正確快取鍵或 Vary。
第七步:用請求序列驗證,不只看一次重新整理
可以在瀏覽器 DevTools 或 curl 中建立以下測試矩陣:
curl -I "$ORIGIN/index.html"
curl -I -H 'If-None-Match: "index-v184"' "$ORIGIN/index.html"
curl -I -H 'Accept-Language: zh-CN' "$ORIGIN/api/catalog"
curl -I -H 'Accept-Language: en-US' "$ORIGIN/api/catalog"先把 ORIGIN 設為待測網站的來源位址。驗證項目包括:
- 首次存取回傳 200、預期回應標頭和驗證器;
- 靜態資源在新鮮期間不請求上游,URL 改變後取得新檔案;
- HTML 條件式請求在未變更時回傳 304,發布後回傳帶新 ETag 的 200;
- 公開 API 的
Age隨 CDN 命中增加,超過共享新鮮期後觸發驗證; - 兩種語言得到不同內容,且回應中包含
Vary; - 購物車回應沒有可被共享快取重用的設定;
- 重播舊 HTML 時,它引用的舊雜湊資源仍回傳 200;
- 開啟「Disable cache」只適合診斷網路請求,不能取代真實快取路徑測試。
還要分別測試一般重新整理、強制重新整理、新分頁、前進/返回,以及有 Service Worker 的路徑。只測試強制重新整理會主動改變請求指令,無法證明一般使用者實際經歷的快取行為。
高品質示範回答
「我不會先背回應標頭,而是對每個回應回答四個問題:能不能存、誰能重用、多久新鮮、過期後怎麼驗證。
帶內容雜湊的 JS 滿足 URL 隨內容改變,所以我會用 public, max-age=31536000, immutable,並把『同一 URL 永不覆蓋』當成發布不變量。入口 HTML 的 URL 固定,我會用 no-cache 加 ETag,讓它可以儲存,但每次正常重用前都要驗證;沒變更就回傳 304,有變更就回傳新 HTML 與新 ETag。no-cache 不是禁止儲存,no-store 才是。
公開目錄允許約 60 秒延遲,我會讓瀏覽器使用 max-age=0,讓 CDN 使用 s-maxage=60。如果業務接受額外 30 秒舊資料換取較低延遲,再加 stale-while-revalidate=30;不能接受就不加。同一個 /api/catalog 依 Accept-Language 回傳不同內容時必須加 Vary: Accept-Language,更可控的方案是把 locale 放進 URL。
購物車明確是使用者私有資料。允許瀏覽器儲存並驗證時用 private, no-cache;資安策略禁止儲存時只用 no-store。我不會讓個人化回應進入共享快取,也不會用高基數 Cookie 當共享快取鍵。
舊 HTML 請求到已刪除 JS,表示發布協定也有問題。我會先上傳新資源,再切換 HTML,並保留舊雜湊資源到舊入口與回滾期間結束。驗證時連續測試首次 200、fresh hit、帶 ETag 的 304、發布後的 200、兩種語言、購物車共享邊界和舊 HTML 重播,而不是只按一次強制重新整理。」
常見錯誤
- 把
no-cache解釋成不快取 → 它允許儲存,只要求重用前驗證 → 禁止儲存時使用no-store,需要 304 時保留no-cache與驗證器。 - 所有資源統一快取一年 → 固定 URL 的 HTML 可能長期指向舊版本 → 只替內容定址資源設定長期快取,入口使用驗證策略。
- 發布後立刻刪除舊雜湊檔案 → 舊分頁、歷史導覽和回滾仍可能引用它 → 先發布資源、後發布入口,並保留舊資源到可達期間結束。
- 只設定 ETag,不設定新鮮度策略 → 快取不知道何時可直接重用,可能產生不必要驗證 → 同時定義 freshness 與 validation。
- 認為 304 沒有成本 → 它節省回應內容,仍有請求往返和伺服器驗證工作 → 依延遲與新鮮度要求決定是否給一段 fresh lifetime。
- 同一 URL 回傳多語言卻不設定
Vary→ 共享快取可能重用錯誤表示 → 把語言放進 URL,或用Vary: Accept-Language納入快取鍵。 - 用
Vary: Cookie隔離購物車 → 高基數快取鍵降低命中並擴大隱私風險 → 使用private或no-store阻止共享重用。 - 事故後新增
no-store就認為舊快取消失 → 新回應標頭無法接觸仍被直接重用的舊副本 → 執行 purge、版本化或等待舊副本過期。 - 只用 DevTools 強制重新整理驗證 → 強制重新整理會改變請求快取指令 → 測試一般導覽、fresh hit、重新驗證、歷史恢復和 Service Worker 路徑。
追問及應對
追問一:如果 CDN 故障,目錄 API 能否繼續回傳舊資料?
先把業務容忍度拆成兩個期間:正常情況最多 60 秒,來源站 5xx 時是否允許更舊。若允許,可評估 stale-if-error 並設定有限秒數;若價格或庫存不能過舊,就讓錯誤明確出現。stale-while-revalidate 處理背景更新延遲,stale-if-error 處理錯誤期間重用,兩者不能混為同一含義。
追問二:強 ETag 與弱 ETag 如何選擇?
強 ETag 表示兩個表示逐位元組相同,適合需要精確範圍請求或位元組一致性的資源。弱 ETag 以 W/ 開頭,表示語意等價但位元組可能不同,適合伺服器端渲染中不重要的格式差異。快取驗證可以使用兩者,但涉及 If-Match 並行控制或範圍請求時,必須重新檢查強比較要求。
追問三:為什麼瀏覽器顯示「from memory cache」或「from disk cache」,卻沒有 304?
資源仍新鮮時可以直接重用,不需要條件式請求,因此不會出現 304。memory 與 disk 是瀏覽器選擇的儲存位置,不是 HTTP 語意本身。判斷行為時應同時查看 Cache-Control、Age、請求是否真的送出和資源目前年齡,不能從 DevTools 的單一標籤反推完整策略。
追問四:加入 Service Worker 後,為什麼修改回應標頭沒有生效?
Service Worker 可能在網路請求前就從 Cache Storage 回傳舊回應,一般 HTTP 快取甚至沒有機會參與。需要檢查 fetch 處理器、Cache Storage 版本、activate 清理和用戶端接管時機。修復時替快取名稱或資源清單加版本,並驗證舊 Service Worker 控制的頁面如何升級。
追問五:如果個人化 HTML 也引用雜湊資源,應如何設定?
HTML 可以使用 Cache-Control: private, no-cache,避免共享快取重用,並在私有快取使用前驗證;雜湊靜態資源若對所有使用者相同,仍可使用公開長期快取。驗證頁面與靜態資源不必共用同一策略,關鍵是依表示是否個人化分別設定邊界。
追問六:如何安全清理舊靜態資源?
從目前和可回滾的 HTML、路由清單與發布紀錄建立引用集合,再加上舊頁面最長可達期間。只有不再被任何可發布入口引用、回滾期間結束,且存取紀錄已無有效請求的雜湊資源,才能進入延遲刪除。清理工作應可中止並保留最近數個發布版本,避免一次誤判破壞回滾。