HTTP 緩存兩大模式
| 模式 | 核心頭部 | 流程 | 適用場景 |
| 強緩存 | `Cache-Control`、`Expires` | 瀏覽器直接讀取本地副本,**不發請求** | 靜態資源(JS/CSS/圖片/字體)、版本化文件 |
| 協商緩存 | `Last-Modified`/`If-Modified-Since`、`ETag`/`If-None-Match` | 瀏覽器發請求詢問,**服務器返回 304 或 200** | 動態頁面、頻繁變更資源、需精確控制 |
關鍵頭部詳解
1. Cache-Control(現代首選)
Cache-Control: max-age=31536000, public, immutable # 靜態資源一年,不可變
Cache-Control: max-age=300, must-revalidate # 動態頁面 5 分鐘,過期必驗證
Cache-Control: no-cache # 每次請求必驗證(協商緩存)
Cache-Control: no-store # 完全禁止緩存(敏感數據)
Cache-Control: private, max-age=600 # 僅瀏覽器緩存,CDN/代理不緩存
- `max-age`:秒數,優先級高於 `Expires`。
- `public`/`private`:是否允許中間代理緩存。
- `must-revalidate`:過期後**必須**向源服務器驗證,不可使用過期副本。
- `immutable`:資源永不變更(配合文件名哈希),瀏覽器甚至不發 304 請求。
2. ETag(實體標籤)
- 強 ETag:`ETag: "abc123"` —— 字節級精確匹配,支援範圍請求。
- 弱 ETag:`ETag: W/"abc123"` —— 語義等價(如壓縮前後)、不支援範圍請求。
- 生成策略:`md5(file_content)`、`hash('crc32b', content)`、`版本號+修改時間`。
3. Last-Modified
- 格式:`Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT`(HTTP-date)。
- 精度 **秒級**,高併發下可能 1 秒內多次修改導致判斷失準,建議配合 ETag 使用。
PHP 實現協商緩存(動態頁面)
<?php
declare(strict_types=1);
function sendNotModified(array $validators = []): never
{
// 發送 304,必須包含原有驗證頭
header('HTTP/1.1 304 Not Modified');
foreach ($validators as $h => $v) {
header("$h: $v", true);
}
// 304 響應**不允許**有 Body
exit;
}
function handleConditionalRequest(string $content, array $options = []): void
{
$etag = $options['etag'] ?? md5($content);
$lastModified = $options['last_modified'] ?? null; // Unix timestamp
// 發送驗證頭
header("ETag: \"$etag\"");
if ($lastModified) {
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
}
// 檢查 If-None-Match (ETag)
$ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
if ($ifNoneMatch) {
// 解析多個 ETag(逗號分隔)
$clientEtags = array_map('trim', explode(',', $ifNoneMatch));
if (in_array($etag, $clientEtags, true) || in_array('*', $clientEtags, true)) {
sendNotModified(['ETag' => "\"$etag\""] + ($lastModified ? ['Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'] : []));
}
}
// 檢查 If-Modified-Since
$ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';
if ($lastModified && $ifModifiedSince) {
$clientTime = strtotime($ifModifiedSince);
if ($clientTime >= $lastModified) {
sendNotModified(['Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT', 'ETag' => "\"$etag\""]);
}
}
// 未命中緩存,發送 200 + 內容
header('Content-Type: text/html; charset=utf-8');
echo $content;
}
// 使用示例
$html = renderTemplate('article', ['id' => 123]);
$lastMod = $article->updated_at; // Unix timestamp
handleConditionalRequest($html, ['last_modified' => $lastMod]);
Nginx 靜態資源完美緩存配置
# 靜態資源(帶哈希文件名)
location ~* \.(?:css|js|png|jpg|jpeg|gif|webp|svg|woff2?|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
add_header X-Cache-Status $upstream_cache_status;
}
# 動態頁面(PHP)
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
# 協商緩存由 PHP 控制,這裡只關閉強緩存
add_header Cache-Control "no-cache, must-revalidate";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# HTML 頁面(可選:短期強緩存 + 協商)
location ~* \.html$ {
expires 5m;
add_header Cache-Control "public, must-revalidate";
try_files $uri $uri/ /index.php$is_args$args;
}
Apache `.htaccess` 對應配置
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType text/html "access plus 5 minutes"
# 靜態資源 immutable
Header set Cache-Control "public, max-age=31536000, immutable"
# HTML 協商緩存
Header set Cache-Control "public, max-age=300, must-revalidate"
# 動態 API 禁止緩存
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
進階:Service Worker 離線緩存(PWA)
// sw.js
const CACHE_NAME = 'app-v1.2.3';
const STATIC_ASSETS = [
'/', '/offline.html',
'/assets/css/main.css',
'/assets/js/app.js',
];
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)));
self.skipWaiting();
});
self.addEventListener('fetch', (e) => {
// 策略:網絡優先,失敗回退緩存
e.respondWith(
fetch(e.request).catch(() => caches.match(e.request))
);
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keys) => Promise.all(
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
))
);
self.clients.claim();
});
調試與驗證清單
| 工具 | 檢查點 |
| Chrome DevTools Network | 查看 `Cache-Control`、`ETag`、`Last-Modified`、`Age`、`X-Cache-Status`;刷新觀察 `304` / `(from disk cache)` / `(from memory cache)` |
| curl -I | `curl -I https://example.com/style.css` 驗證頭部 |
| RedBot | `https://redbot.org/` 自動分析緩存合規性 |
| WebPageTest | 查看 `First Byte`、`Cache Static Content` 評分 |
常見誤區
- 動態頁面設置 `max-age` 長時間 → 用戶看到舊數據;正確:`no-cache, must-revalidate` + ETag/Last-Modified。
- 靜態資源無版本哈希卻用 `immutable` → 更新後瀏覽器永不重新獲取;正確:構建工具生成 `main.a1b2c3.css`。
- 同時發送 `ETag` 和 `Last-Modified` 但邏輯衝突** → HTTP 規範要求**兩者同時滿足**才返回 304,建議**優先 ETag**,Last-Modified 作備選。
- 304 響應帶 Body** → 違反 RFC 7232,導致瀏覽器解析錯誤;304 **絕對不能**有響應體。
- HTTPS 頁面引用 HTTP 靜態資源** → 混合內容阻攔,緩存失效;全站 HTTPS + 相對協議 `//cdn.example.com/...`。
總結
- 靜態資源:文件名含哈希 + `Cache-Control: public, max-age=31536000, immutable` + CDN 緩存,**零請求**。
- 動態頁面:`Cache-Control: no-cache, must-revalidate` + **ETag 優先** + Last-Modified 備選,**協商緩存**返回 304。
- 敏感/個性化頁面:`Cache-Control: private, no-store` + `Vary: Cookie`,**完全不緩存**。
- Nginx/Apache 靜態資源配置** 統一管理,**PHP 僅處理動態協商邏輯**。
正確配置 HTTP 緩存,能將靜態資源命中率提升至 99%+、動態頁面 304 比例超 80%,大幅降低帶寬成本與服務器壓力。