Skip to main content

[Medium] 📄 HTTP Caching

1. What is HTTP caching and why is it important?

什麼是 HTTP 快取?為什麼它很重要?

HTTP 快取是一種在用戶端(瀏覽器)或中間伺服器暫時儲存 HTTP 回應的技術,目的是在後續請求時可以直接使用快取的資料,而不需要再次向伺服器請求。

快取 vs 暫存:有什麼不同?

在繁體中文技術文件中,這兩個詞經常被混用,但實際上有不同的含義:

Cache(快取)

定義:為了效能優化而儲存的資料副本,強調「重複使用」和「加速存取」。

特點

  • ✅ 目的是提升效能
  • ✅ 資料可以被重複使用
  • ✅ 有明確的過期策略
  • ✅ 通常是原始資料的副本

範例

// HTTP Cache - 快取 API 回應
Cache-Control: max-age=3600 // 快取 1 小時

// Memory Cache - 快取計算結果
const cache = new Map();
function fibonacci(n) {
if (cache.has(n)) return cache.get(n); // 重複使用快取
const result = /* 計算 */;
cache.set(n, result);
return result;
}

Temporary Storage(暫存)

定義臨時儲存的資料,強調「暫時性」和「會被清除」。

特點

  • ✅ 目的是臨時保存
  • ✅ 不一定會被重複使用
  • ✅ 生命週期通常較短
  • ✅ 可能包含中間狀態

範例

// sessionStorage - 暫存使用者輸入
sessionStorage.setItem('formData', JSON.stringify(form)); // 關閉分頁即清除

// 暫存檔案上傳
const tempFile = await uploadToTemp(file); // 處理完就刪除
await processFile(tempFile);
await deleteTempFile(tempFile);

對照表

特性Cache(快取)Temporary Storage(暫存)
主要目的效能優化臨時保存
重複使用是,多次讀取不一定
生命週期根據策略決定通常較短
典型用途HTTP Cache, Memory CachesessionStorage, 暫存檔案
英文對應CacheTemp / Temporary / Buffer

實際應用中的區別

// ===== Cache(快取)的使用場景 =====

// 1. HTTP 快取:重複使用 API 回應
fetch('/api/users') // 第一次請求
.then((response) => response.json());

fetch('/api/users') // 第二次從快取讀取
.then((response) => response.json());

// 2. 計算結果快取
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key); // 重複使用
const result = fn(...args);
cache.set(key, result);
return result;
};
};

// ===== Temporary Storage(暫存)的使用場景 =====

// 1. 表單資料暫存(防止意外關閉)
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('formDraft', JSON.stringify(formData));
});

// 2. 上傳檔案暫存
async function handleUpload(file) {
const tempPath = await uploadToTempStorage(file); // 暫存
const processed = await processFile(tempPath);
await deleteTempFile(tempPath); // 用完即刪
return processed;
}

// 3. 中間計算結果暫存
const tempResults = []; // 暫存中間結果
for (const item of items) {
tempResults.push(process(item));
}
const final = combine(tempResults); // 用完就不需要了

在 Web 開發中的應用

// HTTP Cache(快取)- 長期儲存,重複使用
Cache-Control: public, max-age=31536000, immutable
// → 瀏覽器會快取這個檔案一年,重複使用

// sessionStorage(暫存)- 臨時儲存,關閉即清
sessionStorage.setItem('tempData', data);
// → 只在當前分頁有效,關閉就清除

// localStorage(長期儲存)- 介於兩者之間
localStorage.setItem('userPreferences', prefs);
// → 持久化儲存,但不是為了效能優化

為什麼區分這兩個概念很重要?

  1. 設計決策

    • 需要效能優化? → 使用快取
    • 需要臨時保存? → 使用暫存
  2. 資源管理

    • 快取:注重命中率、過期策略
    • 暫存:注重清理時機、容量限制
  3. 面試回答

    • 問「如何優化效能」→ 談快取策略
    • 問「如何處理臨時資料」→ 談暫存方案

在本文中,我們主要討論的是 Cache(快取),特別是 HTTP 快取機制。

快取的好處

  1. 減少網路請求:直接從本地快取讀取,不需要發送 HTTP 請求
  2. 降低伺服器負載:減少伺服器需要處理的請求數量
  3. 加快頁面載入速度:本地快取讀取速度遠快於網路請求
  4. 節省頻寬:減少資料傳輸量
  5. 改善使用者體驗:頁面回應更快,使用更流暢

快取的類型

┌─────────────────────────────────────┐
│ 瀏覽器快取層級 │
├─────────────────────────────────────┤
│ 1. Memory Cache (記憶體快取) │
│ - 最快,容量小 │
│ - 關閉分頁即清除 │
├─────────────────────────────────────┤
│ 2. Disk Cache (磁碟快取) │
│ - 較慢,容量大 │
│ - 持久化儲存 │
├─────────────────────────────────────┤
│ 3. Service Worker Cache │
│ - 開發者完全控制 │
│ - 離線應用支援 │
└─────────────────────────────────────┘

2. What are the HTTP caching strategies?

HTTP 快取策略有哪些?

快取策略分類

HTTP 快取策略
├── 強快取 (Strong Cache)
│ ├── Cache-Control
│ └── Expires
└── 協商快取 (Negotiation Cache)
├── Last-Modified / If-Modified-Since
└── ETag / If-None-Match

1. 強快取(Strong Cache / Fresh)

特點:瀏覽器直接從本地快取讀取,不會向伺服器發送請求。

Cache-Control(HTTP/1.1)

Cache-Control: max-age=3600

常用指令

// 1. max-age:快取有效時間(秒)
Cache-Control: max-age=3600 // 快取 1 小時

// 2. no-cache:需要向伺服器驗證(使用協商快取)
Cache-Control: no-cache

// 3. no-store:完全不快取
Cache-Control: no-store

// 4. public:可被任何快取(瀏覽器、CDN)
Cache-Control: public, max-age=31536000

// 5. private:只能被瀏覽器快取
Cache-Control: private, max-age=3600

// 6. immutable:資源永不改變(配合 hash 檔名)
Cache-Control: public, max-age=31536000, immutable

// 7. must-revalidate:過期後必須向伺服器驗證
Cache-Control: max-age=3600, must-revalidate

Expires(HTTP/1.0,已過時)

Expires: Wed, 21 Oct 2025 07:28:00 GMT

問題

  • 使用絕對時間,依賴客戶端時間
  • 客戶端時間不準確會導致快取失效
  • 已被 Cache-Control 取代

2. 協商快取(Negotiation Cache / Validation)

特點:瀏覽器會向伺服器發送請求,詢問資源是否有更新。

Last-Modified / If-Modified-Since

# 伺服器回應(首次請求)
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

# 瀏覽器請求(後續請求)
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

流程

  1. 首次請求:伺服器回傳 Last-Modified
  2. 後續請求:瀏覽器帶上 If-Modified-Since
  3. 資源未修改:伺服器回傳 304 Not Modified
  4. 資源已修改:伺服器回傳 200 OK 和新資源

ETag / If-None-Match

# 伺服器回應(首次請求)
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 瀏覽器請求(後續請求)
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

優點

  • Last-Modified 更精確
  • 不依賴時間,使用內容 hash
  • 可偵測到秒級以下的變化

Last-Modified vs ETag

特性Last-ModifiedETag
精確度秒級內容 hash,更精確
效能較快需要計算 hash,較慢
適用場景一般靜態資源需要精確控制的資源
優先級高(ETag 優先)

3. How does browser caching work?

瀏覽器快取的運作流程是什麼?

完整快取流程

┌──────────────────────────────────────────────┐
│ 瀏覽器請求資源流程 │
└──────────────────────────────────────────────┘

1. 檢查 Memory Cache

┌───────┴────────┐
│ 找到快取? │
└───────┬────────┘
Yes │ No

2. 檢查 Disk Cache

┌───────┴────────┐
│ 找到快取? │
└───────┬────────┘
Yes │ No

3. 檢查 Service Worker

┌───────┴────────┐
│ 找到快取? │
└───────┬────────┘
Yes │ No

4. 檢查快取是否過期

┌───────┴────────┐
│ 已過期? │
└───────┬────────┘
Yes │ No

5. 使用協商快取驗證

┌───────┴────────┐
│ 資源已修改? │
└───────┬────────┘
Yes │ No (304)

6. 向伺服器請求新資源

┌───────┴────────┐
│ 回傳新資源 │
│ (200 OK) │
└────────────────┘

實際範例

// 首次請求
GET /api/data.json
Response:
200 OK
Cache-Control: max-age=3600
ETag: "abc123"

{ data: "..." }

// ========== 1 小時內再次請求 ==========
// 強快取:直接從本地讀取,不發送請求
// Status: 200 OK (from disk cache)

// ========== 1 小時後再次請求 ==========
// 協商快取:發送驗證請求
GET /api/data.json
If-None-Match: "abc123"

// 資源未修改
Response:
304 Not Modified
(不返回 body,使用本地快取)

// 資源已修改
Response:
200 OK
ETag: "def456"

{ data: "new data" }

4. What are the common caching strategies?

常見的快取策略有哪些?

1. 永久快取策略(適用於靜態資源)

// HTML:不快取,每次都檢查
Cache-Control: no-cache

// CSS/JS(帶 hash):永久快取
Cache-Control: public, max-age=31536000, immutable
// 檔名:main.abc123.js

原理

  • HTML 不快取,確保使用者拿到最新版本
  • CSS/JS 使用 hash 檔名,內容改變時檔名也改變
  • 舊版本不會被使用,新版本會被重新下載

2. 頻繁更新資源策略

// API 資料:短時間快取 + 協商快取
Cache-Control: max-age=60, must-revalidate
ETag: "abc123"

3. 圖片資源策略

// 使用者頭像:中期快取
Cache-Control: public, max-age=86400 // 1 天

// Logo、圖示:長期快取
Cache-Control: public, max-age=2592000 // 30 天

// 動態圖片:協商快取
Cache-Control: no-cache
ETag: "image-hash"

4. 不同資源類型的快取建議

const cachingStrategies = {
// HTML 文件
html: 'Cache-Control: no-cache',

// 帶 hash 的靜態資源
staticWithHash: 'Cache-Control: public, max-age=31536000, immutable',

// 不常更新的靜態資源
staticAssets: 'Cache-Control: public, max-age=2592000',

// API 資料
apiData: 'Cache-Control: private, max-age=60',

// 使用者特定資料
userData: 'Cache-Control: private, no-cache',

// 敏感資料
sensitive: 'Cache-Control: no-store',
};

5. Service Worker caching

Service Worker 快取

Service Worker 提供了最靈活的快取控制,開發者可以完全控制快取邏輯。

基本使用

// 註冊 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// sw.js - Service Worker 檔案
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png',
];

// 安裝事件:快取靜態資源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache);
})
);
});

// 請求攔截:使用快取策略
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// 快取優先策略
return response || fetch(event.request);
})
);
});

// 更新事件:清理舊快取
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});

常見快取策略

1. Cache First(快取優先)

// 適用:靜態資源
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});

2. Network First(網路優先)

// 適用:API 請求
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// 更新快取
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// 網路失敗,使用快取
return caches.match(event.request);
})
);
});

3. Stale While Revalidate(過期重新驗證)

// 適用:需要快速回應但也要保持更新的資源
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});

// 回傳快取,背景更新
return cachedResponse || fetchPromise;
})
);
});

6. How to implement cache busting?

如何實現快取破壞(Cache Busting)?

快取破壞是確保使用者獲取最新資源的技術。

方法 1:檔名 Hash(推薦)

// 使用 Webpack/Vite 等打包工具
// 輸出:main.abc123.js

// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
},
};
<!-- 自動更新引用 -->
<script src="/js/main.abc123.js"></script>

優點

  • ✅ 檔名改變,強制下載新檔案
  • ✅ 舊版本仍可快取,不浪費
  • ✅ 最佳實踐

方法 2:Query String 版本號

<!-- 手動更新版本號 -->
<script src="/js/main.js?v=1.2.3"></script>
<link rel="stylesheet" href="/css/style.css?v=1.2.3" />

缺點

  • ❌ 部分 CDN 不快取帶 query string 的資源
  • ❌ 需要手動維護版本號

方法 3:時間戳

// 開發環境使用
const timestamp = Date.now();
const script = document.createElement('script');
script.src = `/js/main.js?t=${timestamp}`;
document.body.appendChild(script);

用途

  • 開發環境避免快取
  • 不適合生產環境(每次都是新請求)

7. Common caching interview questions

常見快取面試題

題目 1:如何讓 HTML 不被快取?

點擊查看答案
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

或者使用 meta 標籤:

<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />

題目 2:為什麼要使用 ETag 而不只用 Last-Modified?

點擊查看答案

ETag 的優勢

  1. 更精確:可以偵測到秒級以下的變化
  2. 內容驅動:基於內容 hash,而不是時間
  3. 避免時間問題
    • 檔案內容沒變但時間改變(如重新部署)
    • 循環更新的資源(週期性回到相同內容)
  4. 分散式系統:不同伺服器的時間可能不同步

範例

// 檔案內容沒變,但 Last-Modified 改變
// 2024-01-01 12:00 - 部署版本 A(內容:abc)
// 2024-01-02 12:00 - 重新部署版本 A(內容:abc)
// Last-Modified 改變,但內容相同!

// ETag 不會有這個問題
ETag: 'hash-of-abc'; // 永遠一樣

題目 3:from disk cache 和 from memory cache 的區別?

點擊查看答案
特性Memory CacheDisk Cache
儲存位置記憶體(RAM)硬碟
速度極快較慢
容量小(MB 級)大(GB 級)
持久性關閉分頁即清除持久化儲存
優先級高(優先使用)

載入優先順序

1. Memory Cache(最快)
2. Service Worker Cache
3. Disk Cache
4. HTTP Cache
5. 網路請求(最慢)

觸發條件

  • Memory Cache:剛訪問過的資源(如重新整理頁面)
  • Disk Cache:較久前訪問的資源,或檔案較大的資源

題目 4:如何強制瀏覽器重新載入資源?

點擊查看答案

開發階段

// 1. Hard Reload(Ctrl/Cmd + Shift + R)
// 2. 清除快取並重新載入

// 3. 程式碼中加時間戳
const script = document.createElement('script');
script.src = `/js/main.js?t=${Date.now()}`;

生產環境

// 1. 使用檔名 hash(最佳實踐)
main.abc123.js // Webpack/Vite 自動產生

// 2. 更新版本號
<script src="/js/main.js?v=2.0.0"></script>

// 3. 設定 Cache-Control
Cache-Control: no-cache // 強制驗證
Cache-Control: no-store // 完全不快取

題目 5:PWA 離線快取如何實現?

點擊查看答案
// sw.js - Service Worker
const CACHE_NAME = 'pwa-v1';
const OFFLINE_URL = '/offline.html';

// 安裝時快取離線頁面
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
OFFLINE_URL,
'/styles/offline.css',
'/images/offline-icon.png',
]);
})
);
});

// 請求攔截
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
// 網路失敗,顯示離線頁面
return caches.match(OFFLINE_URL);
})
);
}
});

完整 PWA 快取策略

// 1. 快取靜態資源
caches.addAll(['/css/', '/js/', '/images/']);

// 2. API 請求:Network First
// 3. 圖片:Cache First
// 4. HTML:Network First,失敗顯示離線頁面

8. Best practices

最佳實踐

✅ 推薦做法

// 1. HTML - 不快取,確保使用者拿到最新版本
// Response Headers:
Cache-Control: no-cache

// 2. CSS/JS(帶 hash)- 永久快取
// 檔名:main.abc123.js
Cache-Control: public, max-age=31536000, immutable

// 3. 圖片 - 長期快取
Cache-Control: public, max-age=2592000 // 30 天

// 4. API 資料 - 短期快取 + 協商快取
Cache-Control: private, max-age=60
ETag: "api-response-hash"

// 5. 使用 Service Worker 實現離線支援

❌ 避免的做法

// ❌ 壞:HTML 設定長期快取
Cache-Control: max-age=31536000 // 使用者可能看到舊版本

// ❌ 壞:使用 Expires 而不是 Cache-Control
Expires: Wed, 21 Oct 2025 07:28:00 GMT // HTTP/1.0,已過時

// ❌ 壞:完全不設定快取
// 沒有快取頭,瀏覽器行為不確定

// ❌ 壞:對所有資源使用相同策略
Cache-Control: max-age=3600 // 應該根據資源類型調整

快取策略決策樹

是靜態資源?
├─ 是 → 檔名有 hash?
│ ├─ 是 → 永久快取(max-age=31536000, immutable)
│ └─ 否 → 中長期快取(max-age=2592000)
└─ 否 → 是 HTML?
├─ 是 → 不快取(no-cache)
└─ 否 → 是 API?
├─ 是 → 短期快取 + 協商(max-age=60, ETag)
└─ 否 → 根據更新頻率決定

Reference