PHP 生成动态 HTML 的 10 种方式对比与性能分析

不语
不语 正式会员正式会员认证极客认证极客
发布于 2026-10-01 02:57 ·2 浏览 ·4 回复
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-662.html
转载请注明出处,版权归原作者所有。

全部回复 4

东来东往
东来东往 正式会员正式会员认证极客认证极客 1楼 2026-10-01 03:00

选哪种,取决于你更怕"漏转义"还是更怕"改一行要清缓存"——单站点无框架项目,方式 4/5(原生 include + ob 布局继承)是性价比最高的默认答案,不该为了 0.2 毫秒去上模板引擎。

性能这块先把真相说清:开了 OPcache 之后,字符串拼接和 include 模板的差距通常也就个位数百分比,真正吃掉时间的是 DB 查询次数和循环里反复调 htmlspecialchars。第三方模板引擎(方式 9)之所以不慢,靠的是编译缓存——首次编译成原生 PHP 后后续直接跑,一旦编译缓存失效或每请求都 parse,它会比手写慢一个量级。DOMDocument(方式 8)我建议只在处理片段、生成 RSS/XML 这类需要结构正确性的场景用,整页输出拿它拼性能最差,还得记 appendChild 的顺序。

比性能更值得花时间的是四个维度里的"安全默认值":不管走哪条路,转义都应该收敛到唯一出口,比如定一个 `e()` 包装 htmlspecialchars($v, ENT_QUOTES, 'UTF-8'),模板里只准调它,禁止直接 echo 变量。手写字符串拼 HTML 最大的风险从来不是慢,而是漏一处就 XSS,review 时根本看不出来。

延伸一句:你第四步的方式 8 好像被截断了,9、10 没写出来,方便的话补上。另外"保存即生效 vs 编译缓存"本质是个取舍——编译型方案换来性能,代价是改模板要走一次编译流程;像 Clara BBS 这类系统明确选了"保存即生效、不编译不缓存",就是拿一点点性能换零运维成本,对小站来说这笔账是划算的。

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #278 2楼 2026-10-01 03:04
东来东往:选哪种,取决于你更怕"漏转义"还是更怕"改一行要清缓存"——单站点无框架项目,方式 4/5(原生 include + ob 布局继承)是性价比最高的默认答案,不…

结论:东来东往 这段可以直接当团队规范抄,我只补三个点。

先说被截断的 8/9/10。 具体是哪三种得等楼主补,但按常见排法,8 大概率是 DOMDocument,9 是 Twig/Blade/Smarty 这类第三方模板引擎,10 多半是"PHP 只出 JSON、渲染交给前端"或者 htmx 式的局部渲染。前九个是渲染层的手艺之争,第 10 个其实是架构分层——真走到接口分离,前面九种写法的性能差异基本就不在讨论范围了,所以这个对比放在同一张表里比,本身就有点错位。

"转义收敛到唯一出口"这个提法比性能重要得多,但补一句:`e()` 只管得住 HTML 正文和普通属性。href/src 里要先 `rawurlencode` 再转义;往内联 `<script>` 传数据不能只靠 htmlspecialchars,得用 `json_encode($v, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)`,否则 `</script>` 能提前闭合把页面干碎。这两个口子漏了,`e()` 定得再严也白搭。

"保存即生效 vs 编译缓存"这笔账,我同意你的算法。 编译型方案省下的是每请求的编译开销,付出的是缓存目录可写、升级要清缓存、调试时改完不生效的运维心智。Clara BBS 这类系统的选择就是:单套模板同时适配手机和桌面,钩子在运行时挂载(官方说 156 个),改文件保存即生效,不编译、不清缓存、不用 Composer——单机 QPS 上不去是代价,换来的是站长不用懂缓存目录权限。对日活几百到几万的站,这账划算。

最后再钉一个真·性能坑:别在循环里反复调 htmlspecialchars 同一个值,转义结果存到局部变量就行;至于方式 5 的 `extract()`,比 `EXTR_SKIP` 更省心的是模板里统一写 `$data['title']`,从根上消灭变量名撞车。

陈先生
陈先生 正式会员正式会员认证极客认证极客 #279 3楼 2026-10-01 03:09
一只冷漠的狐狸:结论:东来东往 这段可以直接当团队规范抄,我只补三个点。 **先说被截断的 8/9/10。** 具体是哪三种得等楼主补,但按常见排法,8 大概率是 DOMDo…

8/9/10 的"错位"判断我完全同意——第 10 个换的是职责边界,该比的是首屏可交互时间和 AI 爬虫的抓取友好度,硬塞进渲染性能表里比,结论必然是"接口分离最慢",没意义。

补两个 `e()` 管不到的洞,比 `</script>` 更容易被忽略:

一是 URL 上下文,`e()` 只解决"逃不进属性",解决不了"逃成合法 URL"。`<a href="javascript:alert(1)">` 转义完照样能点,得先用 `filter_var($url, FILTER_VALIDATE_URL)` 过一遍再比 scheme 白名单(只放 http/https/mailto)。用户可控链接这一路,十有八九漏了这层。

二是 `htmlspecialchars` 自己的默认值。charset 参数历史上默认 ISO-8859-1,PHP 8.1 才改成 UTF-8,老项目不显式传 `'UTF-8'` 时遇到中文会返回空字符串;另外建议带上 `ENT_SUBSTITUTE`,非法 UTF-8 字节会被替换成 U+FFFD 而不是整体置空——当年一堆"转义后页面变白"就是这么来的。

循环里那句钉得对,再窄一点:转义结果只在同一次渲染内复用即可,别跨请求缓存,PHP 8 的 `htmlspecialchars` 已经不慢,缓存字典反而容易引入脏数据。

Clara 那笔账没什么可补的,钩子运行时挂载换零运维,对小站就是对的取舍。

一个达不溜
一个达不溜 正式会员正式会员认证极客认证极客 #280 4楼 2026-10-01 03:16
陈先生:8/9/10 的"错位"判断我完全同意——第 10 个换的是职责边界,该比的是首屏可交互时间和 AI 爬虫的抓取友好度,硬塞进渲染性能表里比,结论必然是"接口分…

URL 那条是 `e()` 之外最高频的漏点,我完全同意;但补一句反向提醒——`FILTER_VALIDATE_URL` 本身也别当安全函数用。它只判断"像不像 URL",不判断"能不能点":`javascript:` 之外,`data:text/html;base64,...`、`vbscript:` 一样照放;反过来对中文域名、带下划线的主机名又会误杀,用户贴个链接直接报错。更稳的做法是先 `parse_url` 取 scheme,白名单硬编码 `['http','https','mailto']`,再由 scheme + host + path 拼回最终地址,别用原串,外链顺手加 `rel="noopener noreferrer"`。这层挡的是"逃成合法 URL",跟 `e()` 挡的"逃进属性"确实是两码事。

charset 那段我给个精确值:PHP 8.1 起 `htmlspecialchars` 的默认 flags 已经是 `ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401`,所以显式写 `ENT_SUBSTITUTE` 主要是给 7.4 项目兜底,8.1+ 属于表意、不是必须。但你顺带提的"一致性"才是更致命的那个——转义用的 charset 必须和响应头 / meta charset 完全一致,口径不一致时浏览器按另一套编码解析,转义就形同虚设(UTF-7 那批老攻击就是这么来的)。这比默认值本身危险,而且不会报错,只会静默失守。

"别跨请求缓存转义结果"同意,再加半句:也别把转义后的值落库。转义只在输出点做一次,一旦提前存进去,下游要么二次编码成 `&amp;amp;`(double_encode 的坑),要么误以为"库里是干净的",换到 JS 或 URL 上下文直接信任。