PHP 輸出緩沖是很多開發者「知其然不知其所以然」的知識盲區。文章通過一個倒計時輸出示例,揭示了 output_buffering、ob_* 系列函數、flush()、Web 服務器、瀏覽器四層緩沖的交互真相。本文將其系統化整理為完整知識圖譜。
PHP 腳本
│
▼
[1] PHP 內部輸出緩沖 (output_buffering / ob_start)
│ 4096 bytes (默認) 或用戶設置
▼
[2] PHP 發送到 SAPI (Apache mod_php / PHP-FPM / IIS ISAPI)
│ SAPI 可能再緩衝
▼
[3] Web 服務器發送緩衝 (Nginx fastcgi_buffer / Apache buffer / IIS)
│ 通常 4k-8k,可配置
▼
[4] 瀏覽器接收緩衝與渲染策略
│ Content-Length / Transfer-Encoding: chunked
│ 瀏覽器可能等待 <body>、<table> 完整、或特定字節數才渲染
▼
用戶看到畫面
| 名稱 | 層級 | 作用 | 典型用法 |
|---|---|---|---|
output_buffering = 4096 (php.ini) | 1 | 啟用內部緩衝、設置大小 | 生產建議 On (性能);需即時輸出時關閉或用 ob_* 控制 |
ob_start() | 1 | 手動開啟/嵌套緩衝 | 捕獲輸出、壓縮、緩存頁面 |
ob_get_contents() | 1 | 獲取緩衝區內容不清空 | 模板引擎編譯緩存、日誌記錄 |
ob_get_clean() | 1 | 獲取內容並刪除緩衝 | 同 ob_end_clean() 但返回內容 |
ob_end_flush() | 1 | 發送緩衝內容並關閉緩衝 | 結束頁面輸出 |
ob_end_clean() | 1 | 丟棄緩衝內容並關閉緩衝 | 重定向前清理、錯誤頁面替換 |
ob_flush() | 1 | 發送緩衝內容到 SAPI,**保留緩衝** | 長運行腳本分塊輸出 (需配合 flush) |
flush() | 2-3 | 強制 SAPI/WebServer 發送已接收數據到客戶端 | 配合 ob_flush() 實現即時輸出 |
ob_implicit_flush(true) | 1-2 | 每次輸出自動 ob_flush() + flush() | 簡化長腳本輸出代碼 |
<?php
// 情況 A:默認 output_buffering=4096,無任何 flush
for ($i=10; $i>0; $i--) {
echo $i;
sleep(1);
}
// 結果:等待 10 秒,一次性輸出 "10987654321"
// 原因:PHP 緩衝區未滿 (4096),腳本結束才統一發送
// 情況 B:ob_end_clean() 關閉緩衝
ob_end_clean();
for ($i=10; $i>0; $i--) {
echo $i;
flush(); // 僅 flush() 不夠,因 PHP 緩衝已關閉,直接進 SAPI
sleep(1);
}
// 結果:每秒輸出一個數字
// 原因:無 PHP 緩衝,echo 直接進 SAPI,flush() 推送到客戶端
// 情況 C:ob_flush() + flush() 雙重衝刷 (緩衝仍開啟)
for ($i=10; $i>0; $i--) {
echo $i;
ob_flush(); // 將 PHP 緩衝推送到 SAPI
flush(); // 強制 SAPI/WebServer 推送到客戶端
sleep(1);
}
// 結果:每秒輸出一個數字
// 原理:雙重保險,繞過所有服務端緩衝
// 情況 D:ob_implicit_flush(true) 自動模式
ob_implicit_flush(true);
for ($i=10; $i>0; $i--) {
echo $i;
// 內部自動 ob_flush() + flush()
sleep(1);
}
// 結果:每秒輸出 (若 output_buffering=0 或已 ob_end_clean)
// 注意:若 output_buffering>0 且未關閉,ob_implicit_flush 只能自動 ob_flush,仍需手動 flush 或配置 WebServer 無緩衝
即使服務端完美分塊發送,瀏覽器也可能不渲染:
256 bytes 或 <table> 完整才渲染表格內容。<table> 內容常等 </table> 才顯示。Content-Type: text/html; charset=utf-8。<!DOCTYPE html><html><body> 並 ob_flush(); flush();。<div> 流式佈局。EventSource (SSE) / WebSocket / fetch + ReadableStream 推送,前端 JS 渲染,完全繞過瀏覽器 HTML 解析器緩衝。
<?php
// sse.php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // 關閉 Nginx fastcgi_buffering
$i = 10;
while ($i > 0) {
echo "data: {$i}\n\n"; // SSE 格式
ob_flush(); flush();
sleep(1);
$i--;
}
echo "data: [DONE]\n\n";
// 前端
const evt = new EventSource('sse.php');
evt.onmessage = e => {
if (e.data === '[DONE]') { evt.close(); return; }
document.getElementById('counter').textContent = e.data;
};
優勢:**無瀏覽器渲染緩衝問題、自動重連、輕量級、HTTP/2 多路複用友好**。
ob_end_clean() (或 output_buffering=0) → ob_flush() → flush()。output_buffering 提升吞吐,只有進度條、大文件下載、日誌尾跟蹤等場景才關閉/手動控制。