PHP 输出缓冲(Output Buffer)在前端优化中的妙用
学完这篇,你能在不改模板、不装扩展的前提下,只用 PHP 自带的输出缓冲(`ob_*` 系列函数)给站点做 HTML 压缩、资源版本号替换、关键 CSS 内联和整页缓存这四件前端优化的事。
输出缓冲的原理很简单:调用 `ob_start()` 之后,PHP 不再把 `echo` 的内容直接发给浏览器,而是先攒在内存里,等脚本结束时你还有一次机会把这坨 HTML 拿出来改一改再交出去。所谓「妙用」,全在这次机会上。
第一步:先确认当前缓冲层是干净的
新建一个 `test.php` 丢到站点根目录,访问它:
<?php
var_dump(ob_get_level(), ini_get('output_buffering'));
`ob_get_level()` 返回 0 说明干净;返回 1 说明 PHP 自己开了一层。
注意:当 `output_buffering=4096`(宝塔默认)时,系统会自带一层缓冲,你 `ob_start()` 后层级是 2。中间那层不是你开的,别随手 `ob_end_clean()`,否则可能把该输出的内容吞掉。
第二步:在入口挂统一缓冲
无框架系统的好处就是入口唯一。像 Clara BBS 这类轻量 PHP 论坛,全站请求都过 `index.php`,在文件最顶部加一行即可全局生效:
<?php
ob_start('site_output_filter');
回调函数名先占位,下一步再写。
第三步:写 HTML 压缩回调
function site_output_filter(string $html): string
{
if (stripos($html, '</html>') === false) {
return $html; // JSON、图片等非 HTML 输出原样放过
}
$html = preg_replace('/<!--(?!\[if)[\s\S]*?-->/', '', $html); // 去注释
$html = preg_replace('/>\s{2,}</', '> <', $html); // 标签间压缩
return trim($html);
}
这一步通常能把 HTML 体积压掉 10%~25%。
注意:千万不要直接对整篇 HTML 跑 `preg_replace('/\s+/', ' ')`。`<pre>`、`<textarea>`、`<script>` 里的空白和换行是有语义的,JS 里的模板字符串被压平会直接报错。稳妥做法是先按这几个标签 `preg_split` 切块,只处理块外内容。
第四步:顺手替换资源路径、加版本号
同一份 HTML 里做字符串处理,成本几乎为零:
$html = preg_replace_callback('#(href|src)="(/static/[^"]+)"#', function ($m) {
$file = rtrim($_SERVER['DOCUMENT_ROOT'], '/') . $m[2];
$v = is_file($file) ? filemtime($file) : time();
return $m[1] . '="' . $m[2] . '?v=' . $v . '"';
}, $html);
改完静态文件,浏览器看到的 URL 自动变了,缓存自然失效,不用手动清 CDN。
注意:`filemtime()` 有磁盘 IO,页面量大时建议配合缓存(见第六步),不要每个请求都现算。
第五步:关键 CSS 内联,其余延迟加载
把首屏样式读进来内联,主样式表改成异步加载,消除渲染阻塞:
$critical = @file_get_contents(__DIR__ . '/static/css/critical.css');
$html = str_replace('</head>', "<style>{$critical}</style></head>", $html);
$html = preg_replace(
'#<link rel="stylesheet" href="([^"]+)"#',
'<link rel="stylesheet" href="$1" media="print" onload="this.media=\'all\'"',
$html
);
注意:异步加载的样式表必须在 `<noscript>` 里留一份普通 `<link>` 兜底,否则关掉 JS 的用户会看到裸奔页面。
第六步:把渲染结果整页缓存
缓冲最大的价值在这里——你拿到的是「最终 HTML」,可以直接落盘:
$cacheFile = sys_get_temp_dir() . '/page_' . md5($_SERVER['REQUEST_URI']) . '.html';
if (is_file($cacheFile) && time() - filemtime($cacheFile) < 300) {
ob_end_clean();
readfile($cacheFile);
exit;
}
// ...正常渲染...
$html = ob_get_clean();
file_put_contents($cacheFile, $html);
echo $html;
如果你的系统已经提供了缓存封装(很多轻量系统会带 `Cache::remember` 之类的公共设施),直接套用即可,别再造轮子。
注意:只缓存游客可见、未登录、无 CSRF token 的页面。把带 token 的 HTML 缓存下来发给别人,等于把防护关掉了。
第七步:别让缓冲和 gzip、flush 打架
两个坑:
- 双重压缩:PHP 侧的 `ob_gzhandler` 和 Nginx/宝塔面板的 gzip 同时开,会压两遍甚至报错。建议压缩统一交给 Web 服务器,PHP 侧不开。
- 分块输出:`ob_flush() + flush()` 能让浏览器提前收到首屏 HTML,但会中断 gzip、丢掉 `Content-Length`。首屏确实慢再考虑,别默认加。
注意:`header()`、`setcookie()` 必须在缓冲内容真正发出之前调用。缓冲给了你反悔的时间,但别把 `header()` 拖到 `ob_end_flush()` 之后。
第八步:出错时清干净,别吐半截页面
register_shutdown_function(function () {
if ($e = error_get_last()) {
while (ob_get_level()) { ob_end_clean(); }
// 这里再输出统一错误页
}
});
注意:`ob_get_level() > 0` 时,`ob_end_clean()` 只关最内层。要清到底就用 `while` 循环,否则嵌套层级多的时候会残留。
小结
- `ob_start()` 是「拿到最终 HTML」的唯一时机,所有前端优化都建立在这上面;
- 压缩 HTML 时避开 `<pre>`/`<textarea>`/`<script>`,别用粗暴的全局空白正则;
- 资源加版本号、关键 CSS 内联、异步样式兜底,放在同一个回调里一次做完;
- 整页缓存只缓存游客页,带 token 的内容一律不落盘;
- gzip 交给 Web 服务器,PHP 侧不要重复开;`header()` 一定在输出前调用;
- 出错时用 `while (ob_get_level()) ob_end_clean();` 彻底清空,避免半截页面。
转载请注明出处,版权归原作者所有。
见习用户






