使用 header('Content-Type: image/jpeg') 直接輸出 GD 生成的圖片(驗證碼、圖表、水印圖)時,若無緩存控制,瀏覽器每次請求都會重新下載,造成頻寬浪費與伺服器壓力。本文詳解 HTTP 緩存頭的正確配置,實現「首次下載、後續讀取緩存、內容更新自動失效」的完整流程。
| Header | 作用 | 範例值 |
|---|---|---|
Cache-Control | 控制緩存機制,private 僅客戶端緩存,max-age 秒數 | private, max-age=10800(3小時) |
Pragma | HTTP/1.0 相容性,private 同 Cache-Control | private |
Expires | 絕對過期時間(RFC 822 格式) | Wed, 21 Oct 2025 07:28:00 GMT |
Last-Modified | 資源最後修改時間,配合 If-Modified-Since 實現 304 | gmdate('D, d M Y H:i:s', $mtime) . ' GMT' |
ETag | 實體標籤(檔案雜湊/版本號),強驗證器 | md5_file($image_path) |
<?php
class ImageCacheResponse {
private $imagePath;
private $mimeType;
public function __construct(string $imagePath) {
$this->imagePath = $imagePath;
$this->mimeType = $this->getMimeType();
}
public function send(): void {
if (!file_exists($this->imagePath)) {
http_response_code(404);
exit('Image not found');
}
$mtime = filemtime($this->imagePath);
$etag = md5_file($this->imagePath);
$lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
// 發送緩存控制頭
header('Cache-Control: private, max-age=86400, must-revalidate'); // 1天
header('Pragma: private');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
header('Last-Modified: ' . $lastModified);
header('ETag: "' . $etag . '"');
header('Content-Type: ' . $this->mimeType);
header('Content-Length: ' . filesize($this->imagePath));
// 條件請求處理
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$clientTime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if ($clientTime === $mtime) {
header('HTTP/1.1 304 Not Modified');
header('Last-Modified: ' . $lastModified);
exit;
}
}
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
$clientEtag = str_replace('"', '', $_SERVER['HTTP_IF_NONE_MATCH']);
if ($clientEtag === $etag) {
header('HTTP/1.1 304 Not Modified');
header('ETag: "' . $etag . '"');
exit;
}
}
// 輸出圖片內容
readfile($this->imagePath);
}
private function getMimeType(): string {
$ext = strtolower(pathinfo($this->imagePath, PATHINFO_EXTENSION));
return match($ext) {
'jpg', 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
default => 'application/octet-stream'
};
}
}
// 使用範例
$image = new ImageCacheResponse(__DIR__ . '/captcha.jpg');
$image->send();
若圖片即時生成(無實體檔案),以內容雜湊作為 ETag,並儲存生成結果到快取目錄:
// 生成縮圖並快取
function outputThumbnail(string $src, int $maxW, int $maxH): void {
$cacheDir = __DIR__ . '/cache/thumbs/';
$cacheKey = md5($src . $maxW . $maxH . filemtime($src));
$cacheFile = $cacheDir . $cacheKey . '.jpg';
if (!file_exists($cacheFile)) {
// 生成縮圖邏輯...
imagejpeg($dst, $cacheFile, 90);
}
(new ImageCacheResponse($cacheFile))->send();
}
session_start() 會發送 Cache-Control: no-cache,圖片輸出前務必 session_write_close() 或避開 Sessionmax-age=3600,CDN 設定 s-maxage=86400,利用 Cache-Control: private, s-maxage=86400 區分header('Cache-Control: no-store, no-cache, must-revalidate') 強制每次重新生成Content-Disposition: inline; filename*=UTF-8'' . rawurlencode($filename)| 策略 | 首次請求 | 二次請求(304) | 頻寬節省 |
|---|---|---|---|
| 無緩存頭 | 200 OK / 45KB | 200 OK / 45KB | 0% |
| Expires + Last-Modified | 200 OK / 45KB | 304 / 0.2KB | 99.5% |
| ETag + Cache-Control | 200 OK / 45KB | 304 / 0.2KB | 99.5% |
正確配置 HTTP 緩存,不僅節省頻寬,更能大幅降低伺服器 CPU/IO 壓力,是高效能 Web 應用的基礎功夫。