網頁傳輸體積直接影響首屏加載速度,尤其在移動網絡、大文本內容 (HTML/JSON/CSS/JS) 場景下,啟用 Gzip/Deflate 壓縮可縮減 70-90% 體積。本文提供生產級 PHP 輸出壓縮方案,涵蓋瀏覽器能力檢測、壓縮等級控制、輸出緩衝管理、邊緣情況處理。
Accept-Encoding: gzip, deflate, br 聲明支援。ob_start() → 內容生成完畢 → ob_get_contents() 獲取 → gzencode()/gzdeflate() 壓縮 → 發送 Content-Encoding 標頭 → 輸出壓縮數據。<?php
class GzipOutput {
private int $level;
private bool $debug;
private bool $started = false;
public function __construct(int $level = 6, bool $debug = false) {
$this->level = max(1, min(9, $level)); // 1-9
$this->debug = $debug;
}
// 檢測瀏覽器支援的編碼
public function getSupportedEncoding(): ?string {
if (headers_sent()) return null; // 已發送標頭,無法壓縮
$accept = $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '';
// 優先順序:gzip > deflate > identity
if (stripos($accept, 'gzip') !== false) return 'gzip';
if (stripos($accept, 'deflate') !== false) return 'deflate';
return null;
}
// 啟動壓縮輸出緩衝
public function start(): bool {
if ($this->started) return true;
$encoding = $this->getSupportedEncoding();
if (!$encoding) return false; // 不支援,正常輸出
// 註冊關閉回調,在腳本結束時自動壓縮輸出
ob_start([$this, 'compressCallback']);
$this->started = true;
return true;
}
// ob_start 回調函數
public function compressCallback(string $buffer): string {
if ($buffer === '') return $buffer;
$encoding = $this->getSupportedEncoding();
if (!$encoding) return $buffer; // 理論上不會發生
// 壓縮
$compressed = $encoding === 'gzip'
? gzencode($buffer, $this->level, FORCE_GZIP)
: gzdeflate($buffer, $this->level);
// 設置標頭 (僅第一次調用時有效)
if (!headers_sent()) {
header('Content-Encoding: ' . $encoding);
header('Vary: Accept-Encoding'); // 關鍵:告知代理按 Accept-Encoding 緩存不同版本
// Content-Length 由 PHP 自動計算 (若無手動設置)
}
// 調試信息 (HTML 註釋)
if ($this->debug) {
$ratio = strlen($buffer) ? round(100 * strlen($compressed) / strlen($buffer), 1) : 0;
$buffer .= "\n<!-- Gzip: {$encoding}, Level: {$this->level}, Ratio: {$ratio}% -->\n";
}
return $compressed;
}
// 手動刷新 (長運行腳本中段輸出)
public function flush(): void {
if ($this->started) {
ob_flush();
flush();
}
}
}
// 使用示例:在腳本最開頭調用
require 'GzipOutput.php';
$gzip = new GzipOutput(level: 6, debug: true);
$gzip->start();
// 後續正常輸出 HTML/JSON
echo '<html><body><h1>Hello Compressed World!</h1></body></html>';
// 腳本結束自動觸發 compressCallback 輸出壓縮數據
?>
| 問題 | 原因 | 解決 |
|---|---|---|
| 壓縮後瀏覽器顯示亂碼/下載文件 | 未發送 Content-Encoding 或發送錯誤 | 確保 header() 在任何輸出前調用;回調中檢查 headers_sent() |
| 圖片/PDF/二進制輸出被壓縮導致損壞 | 對非文本內容啟用壓縮 | 僅對 text/html, application/json, text/css, application/javascript 等啟用;if (stripos($contentType, 'text/') === 0 || in_array($contentType, [...])) |
| 已開啟 zlib.output_compression 衝突 | PHP 內建壓縮與手動壓縮雙重疊加 | 二選一:ini_set('zlib.output_compression', 'Off') 或直接用內建 |
| 代理/CDN 緩存錯誤版本 | 缺少 Vary: Accept-Encoding | 必須發送 header('Vary: Accept-Encoding') |
| 大文件記憶體耗盡 | ob_get_contents() 複製整個輸出到記憶體 | 分塊輸出 + ob_flush();或改用 Nginx gzip on 卸載到 Web 伺服器層 |
# 推薦:Nginx 層統一處理 (性能最佳、零 PHP 開銷)
# nginx.conf
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_disable "msie6";
# PHP 層僅處理動態生成的、Nginx 未覆蓋的場景 (如 JSON API、流式輸出)
# 或直接關閉 PHP 壓縮,完全交給 Nginx
ini_set('zlib.output_compression', 'Off');
<?php
// php.ini 或 .user.ini 或運行時 (必須在輸出前)
ini_set('zlib.output_compression', 'On');
ini_set('zlib.output_compression_level', '6');
// 自動檢測 Accept-Encoding、設置 Content-Encoding/Vary、壓縮輸出
echo '這段內容將自動被壓縮';
?>
優點:零代碼侵入、自動處理邊緣情況。缺點:無法精細控制 (如排除二進制、動態調整等級)、調試困難。
| 方案 | 壓縮後大小 | 壓縮比 | CPU 開銷 | 實現複雜度 |
|---|---|---|---|---|
| Nginx gzip (推薦) | ~8 KB | 84% | 極低 (C 級) | 配置一次 |
| PHP zlib.output_compression | ~8 KB | 84% | 低 | 一行配置 |
| PHP 手動 ob_start + gzencode | ~8 KB | 84% | 中 (用戶態複製) | 中 |
| 無壓縮 | 50 KB | 0% | 0 | 0 |
「壓縮是免費的性能提升」。生產環境首選 Nginx/Apache 層統一開啟 Gzip,覆蓋所有靜態/動態資源;PHP 層僅在無法控制 Web 伺服器配置、或需針對特定動態流 (如 Server-Sent Events、流式 JSON) 時使用手動 ob_start 方案。記住 Vary: Accept-Encoding,避免代理緩存污染。