直接用 PHP 輸出圖片(如驗證碼、水印圖、動態圖表)時,若不控制緩存,每次請求都會觸發 PHP 執行,消耗 CPU 與帶寬。本文系統講解 Cache-Control、Expires、Last-Modified、ETag 四大緩存頭部的協作機制,提供可直接生產使用的綜合範例。
| 頭部 | 類型 | 作用 | 優先級 |
|---|---|---|---|
Cache-Control | 強緩存 | 指定 max-age、private/public、no-store 等指令 | 最高 |
Expires | 強緩存 | HTTP/1.0 兼容,絕對過期時間(GMT) | 次之 |
Last-Modified | 協商緩存 | 資源最後修改時間,配合 If-Modified-Since | 較低 |
ETag | 協商緩存 | 資源指紋(哈希/版本號),配合 If-None-Match,強於 Last-Modified | 最低 |
<?php
// ================= 1. 緩存頭部(必須在任何輸出前) =================
$cacheSeconds = 86400 * 2; // 2 天
$etag = '"' . md5(__FILE__ . $imagePath) . '"'; // 基於文件路徑+腳本生成弱 ETag
// 強緩存:瀏覽器 2 天內不發請求
header("Cache-Control: private, max-age=$cacheSeconds, must-revalidate");
header("Expires: " . gmdate('D, d M Y H:i:s', time() + $cacheSeconds) . ' GMT');
// 協商緩存標識
header("Last-Modified: " . gmdate('D, d M Y H:i:s', filemtime($imagePath)) . ' GMT');
header("ETag: $etag");
// ================= 2. 處理協商緩存請求(304 Not Modified) =================
// 情況 A:瀏覽器發送 If-Modified-Since
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$clientTime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
$fileTime = filemtime($imagePath);
if ($clientTime >= $fileTime) {
header('HTTP/1.1 304 Not Modified');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $fileTime) . ' GMT');
header("ETag: $etag");
exit;
}
}
// 情況 B:瀏覽器發送 If-None-Match(ETag 優先級更高)
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
$clientEtag = trim($_SERVER['HTTP_IF_NONE_MATCH'], '"');
if ($clientEtag === trim($etag, '"')) {
header('HTTP/1.1 304 Not Modified');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($imagePath)) . ' GMT');
header("ETag: $etag");
exit;
}
}
// ================= 3. 正常輸出圖片 =================
$imagePath = 'test.jpg'; // 來源圖片
$maxW = 200; $maxH = 200;
list($srcW, $srcH) = getimagesize($imagePath);
// 計算等比縮放尺寸
if ($srcW <= $maxW && $srcH <= $maxH) {
$dstW = $srcW; $dstH = $srcH;
} elseif ($srcW / $maxW > $srcH / $maxH) {
$dstW = $maxW;
$dstH = (int)round($srcH * $maxW / $srcW);
} else {
$dstH = $maxH;
$dstW = (int)round($srcW * $maxH / $srcH);
}
$src = imagecreatefromjpeg($imagePath);
$dst = imagecreatetruecolor($dstW, $dstH);
imagecopyresampled($dst, $src, 0, 0, 0, 0, $dstW, $dstH, $srcW, $srcH);
header('Content-Type: image/jpeg');
header('Content-Length: ' . ob_get_length()); // 若開啟 OB
imagejpeg($dst, null, 90); // 直接輸出,質量 90
imagedestroy($src);
imagedestroy($dst);
Cache-Control: max-age=172800 告訴瀏覽器「2 天內直接讀本地緩存,不發請求」。這是性能收益最大的一步。
強緩存過期後,瀏覽器會攜帶 If-Modified-Since 和/或 If-None-Match 發請求。服務端比對:
304 Not Modified(無 Body,極快)ETag 比 Last-Modified 更可靠:秒級修改、多服務器同步、文件內容未變但時間變等場景,ETag(基於內容哈希)不會誤判。
private:僅瀏覽器緩存,CDN/代理不緩存(適合用戶專屬圖片、驗證碼)。public:允許 CDN/代理緩存(適合公共靜態資源、水印圖)。$response->clearHeaders()。ob_get_length() 在無 OB 時返回 false,Content-Length 可選(Chunked Transfer 可自動處理)。md5_file() 讀磁盤:用 md5($path . filemtime($path) . filesize($path)) 或版本號。| 錯誤 | 後果 | 修正 |
|---|---|---|
| Header 後有空白/換行輸出 | Cannot modify header information | 確保 header() 前無任何 echo/HTML/BOM |
用 date() 而非 gmdate() 生成 GMT 時間 | 時區偏移導致緩存判斷錯誤 | 統一 gmdate('D, d M Y H:i:s') . ' GMT' |
| ETag 未加雙引號 | 不符合 RFC 7232,瀏覽器可能忽略 | header("ETag: \"$hash\"") |
同時發送 no-cache 和 max-age | 指令衝突,行為未定義 | 按需二選一 |
PHP 輸出圖片的完整緩存策略 = 強緩存 + 協商緩存雙層保障。代碼模板已處理:
Cache-Control + Expires 控制強緩存期Last-Modified + ETag 提供協商緩存標識If-None-Match 再檢查 If-Modified-Since304 即刻退出,節省圖片處理 CPU將此模板封裝為 ImageResponse::send($path, $maxAge) 靜態方法,即可在全專案複用,顯著降低圖片類接口的平均響應時間與帶寬成本。