学完这篇,你能弄清 PHP 输出动态 HTML 的 10 种常见写法各自的代价,并知道在你的项目里该选哪一种。
第一步:先把评判标准定下来
别急着比"谁快",先明确四个维度:开发速度(写起来烦不烦)、运行开销(CPU 与内存)、安全默认值(转义是否容易漏)、生效方式(改一次要不要清缓存)。同一个项目里,这四项的权重往往比"快 0.2 毫秒"重要得多。
第二步:字符串层三种写法(方式 1-3)
方式 1:内联 echo/字符串拼接
$html = '<h1>' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</h1>';
最快、零依赖,但 HTML 一长就变成引号地狱。
方式 2:Heredoc / Nowdoc
$html = <<<HTML
<div class="post">
<h1>{$safeTitle}</h1>
</div>
HTML;
可读性好,插值方便,代价是每个变量要事先转义好,Heredoc 里不能写复杂表达式。
方式 3:sprintf / vsprintf 占位
$html = sprintf('<a href="%s">%s</a>', $url, $name);
适合小片段复用,元素一多就成了"参数顺序记不住"的灾难。
注意:以上三种都必须手动调 `htmlspecialchars($v, ENT_QUOTES, 'UTF-8')`。漏一处就是 XSS,这也是手写字符串最大的风险点。
第三步:原生模板包含三种写法(方式 4-6)
方式 4:include 原生 PHP 模板
把 HTML 写成 `.php` 文件,内部用 `<?= htmlspecialchars($title) ?>` 输出。这是无框架 PHP 系统最主流的做法。
方式 5:输出缓冲 + extract
function render(string $tpl, array $data): string {
extract($data, EXTR_SKIP);
ob_start();
include $tpl;
return ob_get_clean();
}
调用方只管传数组,模板里直接用变量名,写起来最舒服。
注意:`extract()` 会把变量灌进当前作用域。如果模板里出现 `$db`、`$config` 这类名字,可能覆盖你的全局变量——一定要带 `EXTR_SKIP`,或干脆改成 `$data['title']` 显式取值。
方式 6:字符串占位替换
$html = str_replace(['{{title}}', '{{body}}'], [$title, $body], $tpl);
上手极快,但模板越大、占位符越多,线性扫描成本越高,也不支持循环和条件。
第四步:布局继承与 DOM 拼装(方式 7-8)
方式 7:ob_start 做布局/区块继承
ob_start();
include $view; // 子模板内容
$content = ob_get_clean();
include $layout; // 布局里 echo $content
用嵌套缓冲实现"母版页 + 内容块",不需要任何引擎,成本只比裸 include 高一点点。
方式 8:DOMDocument 拼装
$doc = new DOMDocument();
$h1 = $doc->createElement('h1', $title);
$doc->appendChild($h1);
echo $doc->saveHTML();
天然防转义错误,但性能差一个数量级,而且对 `<br>`、`<img>` 这类 HTML5 空元素和片段解析容易出坑。只推荐用于生成 XML/RSS,不适合常规页面。
第五步:模板引擎与前后端分离(方式 9-10)
方式 9:Twig / Blade / Smarty
语法干净、自动转义、有编译缓存。代价是引入 Composer 依赖,第一次渲染要编译模板,缓存目录必须可写。
方式 10:PHP 只输出 JSON,前端渲染
PHP 侧只剩 `json_encode()`,服务端最省,但首屏依赖 JS,SEO 需要额外处理。
注意:如果你的系统强调"改完即生效、不需要清缓存"(不少无框架轻量 PHP 系统就是这条路线,像 Clara BBS 就明确要求保存即生效、无编译缓存),那么带编译缓存的引擎反而会增加运维负担——不是引擎不好,是路线不匹配。
第六步:自己跑一次压测,别信别人的数字
$start = hrtime(true);
for ($i = 0; $i < 10000; $i++) {
ob_start();
include __DIR__ . '/tpl/post.php';
$html = ob_get_clean();
}
printf("10000 次渲染耗时 %.1f ms\n", (hrtime(true) - $start) / 1e6);
换成其他写法,保持模板内容一致,逐个跑。下面是常见的量级参考(同一台机器、同一模板,仅供排序参考,不是标准答案):
| 方式 | 相对耗时 | 说明 |
|---|
| 内联拼接 / Heredoc | 1.0x | 基线 |
| include 原生模板 | 约 1.1x | 有文件 IO,开 opcache 后更低 |
| ob + extract | 约 1.2x | 多一层缓冲与作用域注入 |
| 布局继承 | 约 1.3x | 多次缓冲嵌套 |
| sprintf | 约 1.3x | 参数多时更明显 |
| str_replace 占位 | 约 1.6x | 模板越大越慢 |
| 模板引擎(有编译缓存) | 约 1.5-2x | 首次渲染含编译 |
| 模板引擎(无缓存) | 约 3-8x | 每次都要解析 |
| DOMDocument | 约 8-15x | 仅推荐生成 XML/RSS |
| JSON + 前端渲染 | PHP 侧约 0.3x | 首屏依赖 JS |
压测时记得固定变量:模板内容一致、数据量一致、`opcache` 状态一致,跑三次取中位数。
第七步:怎么选
- 页面少、追求极致快:方式 1-3。
- 常规站点、想好维护:方式 4-5 + 方式 7,够用一辈子。
- 团队协作、模板复杂:方式 9,但要接受编译缓存与依赖。
- 只做接口、前端独立:方式 10。
- 生成 XML:方式 8。
小结
- 排序大致是:字符串拼接 ≈ 原生模板 < 占位替换 < 模板引擎(有缓存) < 模板引擎(无缓存) < DOM。
- 真正的性能大头通常在数据库查询和文件 IO,模板层优化空间远小于这两项——先优化 SQL,再纠结模板。
- 无论选哪种,每个动态输出点都要转义,这比性能重要。
- 选型看路线:强调"保存即生效、零依赖"就手写原生模板;强调开发体验和团队规范就上引擎。
- 数字一定要自己压测,环境(opcache、磁盘、PHP 版本)差异比写法差异还大。