前几天升级 PHP 8.5 之后,我一直在继续处理 WordPress 站点上的兼容性与性能问题。
此前已经整理过一篇与 PHP 8.5、PHP-FPM 调整有关的记录:
这一次最初关注的仍然是 PHP 8.5,但排查到最后才发现,真正导致首页动态生成异常缓慢的,并不是 PHP 8.5,也不是 PHP-FPM 参数不足,更不是服务器 CPU 或内存不够。
最终的问题集中在了一个非常具体的位置:
Post Views Counter 的“热门文章(Most Viewed Posts)”排行榜查询。
优化完成之后:
- 中文动态首页从约 18.6~18.8 秒降低到 0.83~0.89 秒
- 英文动态首页稳定在 0.87~0.96 秒
- 排行榜文章、顺序、浏览量均保持一致
- PHP-FPM 不再因为这两个首页产生超过 3 秒的慢请求
这个结果比最初预期的“尽量控制到 10 秒以内”要好得多。
一、最开始,我以为问题可能出在 PHP 8.5
我的 WordPress 当前运行环境包括:
- WordPress 7.0.3
- PHP 8.5.9
- Nginx 1.30.4
- Redis 8.10.0
- MySQL 5.7.32
- Twenty Twenty-Five 主题
- Polylang
- W3 Total Cache
- Post Views Counter 1.7.13
服务器本身配置不高,但检查系统资源以后,并没有发现明显压力。
内存还有约 2 GB 可用,Swap 基本没有被使用,系统 Load 也很低。
PHP-FPM 则已经采用动态进程管理,并配置了:
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500
request_slowlog_timeout = 3s
slowlog = /usr/local/php/var/log/slow.log
OPcache 同样已经正常启用。
因此,如果一个 WordPress 首页需要接近 20 秒才能动态生成,仅仅继续增加 PHP-FPM worker 数量并不能解释这个问题。
二、必须先绕过 CDN 和页面缓存,否则测试结果没有意义
我的中文站使用 EdgeOne,英文站使用 Cloudflare,同时源站又启用了 W3 Total Cache。
因此,直接访问:
看到页面打开很快,并不能证明 PHP 动态执行速度正常。
如果 CDN 或 W3TC 已经命中缓存,请求可能根本没有真正执行完整的 WordPress 首页生成过程。
还有一个容易误判的问题。
此前我会通过给首页增加随机 Query String,例如:
/?random=123
尝试制造缓存 MISS。
但我的 EdgeOne 缓存规则本身会忽略 Query String,而且回源时还会移除 Query String。
因此这种测试方法实际上并不可靠。
后来改为直接把域名解析到本机源站:
curl --resolve www.shuijingwanwq.com:443:127.0.0.1 ...
这样可以完全绕过 EdgeOne。
与此同时,再添加一个模拟 WordPress 登录状态的 Cookie:
wordpress_logged_in_swq_perf_test=1
让 W3 Total Cache 放弃 Page Cache。
这样测到的才是真正的:
Nginx → PHP-FPM → WordPress → 数据库 → 动态页面生成
三、真实动态首页竟然稳定需要约 19 秒
在绕过 CDN 和 W3TC 页面缓存之后,连续测试中文首页:
第一次:
TTFB: 18.607824 秒
第二次:
TTFB: 18.820124 秒
这已经不是偶发抖动。
动态首页确实稳定需要接近 19 秒。
作为对照,不强制绕过 W3TC 时:
- 第一次约 19 秒
- 第二次只需要约 0.016 秒
也就是说,第二次已经直接命中了页面缓存。
缓存只是把问题隐藏起来了。
真正的 WordPress 动态首页仍然非常慢。
四、PHP-FPM slowlog 把目标指向 Post Views Counter
由于之前已经给 PHP-FPM 设置:
request_slowlog_timeout = 3s
所以接下来直接查看 slowlog。
连续几次慢请求最终都停在类似的调用链:
mysqli_query()
wpdb::_do_query()
wpdb::query()
wpdb::get_results()
WP_Query::get_posts()
get_posts()
pvc_get_most_viewed_posts()
pvc_most_viewed_posts()
most_viewed_posts_render_callback()
对应插件:
wp-content/plugins/post-views-counter/
这时排查范围已经缩得很小了。
首页本身有两种 Post Views Counter 使用场景:
一种是在普通文章列表中显示每篇文章的浏览量。
另一种是在侧边栏显示:
Most Viewed Posts / 热门文章排行榜
最终真正有问题的是第二种。
五、首页排行榜到底执行了什么 SQL?
Post Views Counter 获取“总浏览量最高的 10 篇文章”时,核心 SQL 类似:
SELECT DISTINCT
wp_posts.*,
SUM(COALESCE(pvc.count, 0)) AS post_views
FROM wp_posts
LEFT JOIN wp_post_views pvc
ON pvc.id = wp_posts.ID
AND pvc.type = 4
WHERE wp_posts.post_type = 'post'
AND wp_posts.post_status = 'publish'
GROUP BY wp_posts.ID
HAVING post_views > 0
ORDER BY post_views DESC, wp_posts.post_date DESC
LIMIT 0, 10;
中文首页因为还有 Polylang,所以实际查询中还会加入语言 taxonomy 条件。
一开始我怀疑:
wp_post_views数据量是不是太大- 索引是不是缺失
- Polylang JOIN 是不是太慢
但进一步检查后发现这些都不是主要原因。
wp_post_views 大约只有 15 万多行,而且已经存在相关复合索引。
EXPLAIN 也能够看到索引正在被使用。
真正值得注意的是:
SELECT wp_posts.*
以及后面的:
GROUP BY
ORDER BY
SUM()
六、真正的突破:不是 GROUP BY 本身慢,而是 wp_posts.*
随后做了三个直接数据库性能测试。
原始排行榜查询
保留:
wp_posts.*
结果:
18.305059 秒
同时数据库状态显示:
Created_tmp_tables +1
Created_tmp_disk_tables +1
也就是说:
这个查询创建了磁盘临时表。
只选择排名真正需要的字段
随后把 SELECT 缩减为:
- ID
- post_date
- post_views
排行榜逻辑、JOIN、语言过滤、GROUP BY 和 ORDER BY 全部保持不变。
结果:
0.022080 秒
并且:
Created_tmp_tables +1
Created_tmp_disk_tables +0
这一次虽然仍然创建临时表,但已经不再落盘。
再根据最终 10 个 ID 获取完整文章
然后额外执行一次:
SELECT *
FROM wp_posts
WHERE ID IN (...)
只获取排行榜最终选中的 10 篇文章。
耗时:
0.003841 秒
因此两阶段查询加起来不过几十毫秒。
而排行榜前 10 名及其浏览量与原查询完全一致。
七、为什么 wp_posts.* 会把查询从 22 毫秒拖到 18 秒?
这里就能解释整个问题了。
WordPress 的 wp_posts 并不是一张只有标题和 ID 的轻量表。
其中包含:
post_contentpost_excerptpost_passwordpost_title- GUID
- 以及其他大量字段
其中尤其重要的是:
post_content
post_excerpt
它们属于 TEXT/LONGTEXT 一类字段。
当前数据库为 MySQL 5.7。
原始查询把完整:
wp_posts.*
与:
SUM()
GROUP BY
ORDER BY
组合到一起。
最终 MySQL 需要把包含大型文本字段的数据参与中间聚合和排序,内部临时表因此落到磁盘。
真正需要排名的明明只有:
文章 ID + 浏览量 + 日期
但数据库却在排序阶段搬运整篇文章正文。
这就是 18 秒消耗的核心来源。
八、两阶段查询才是更合理的结构
从实际用途来看,热门文章排行榜完全可以拆成两步。
第一阶段只负责:
找出哪些文章应该进入 Top 10。
所需数据很少:
ID
post_date
post_views
完成排序后得到最终 10 个 ID。
第二阶段再获取:
这 10 篇文章完整的 WP_Post。
这样即使排行榜最终需要显示:
- 标题
- 浏览量
- 摘要
- 作者
- 缩略图
也没有必要让这些字段提前参与几千篇文章的 GROUP BY 和 ORDER BY。
这也是为什么:
第一阶段选择哪些字段,与最终网页显示哪些字段,本来就应该是两个不同的问题。
九、第一次 MU Plugin 实现其实失败了
我不准备直接修改 Post Views Counter 插件源码。
原因很简单:
插件以后升级时,修改内容会被覆盖。
因此决定暂时使用一个 MU Plugin:
wp-content/mu-plugins/swq-pvc-most-viewed-optimization.php
但第一版实现走了一条看起来很自然、实际上有问题的路线:
$args['fields'] = 'ids';
思路是让 WordPress 第一阶段只返回 ID。
结果查询确实快了:
elapsed=0.003310 seconds
但是:
count=0
排行榜直接空了。
进一步捕获实际 SQL 后发现:
SELECT DISTINCT wp_posts.ID
...
GROUP BY ...
HAVING post_views > 0
ORDER BY post_views DESC
问题马上就清楚了。
Post Views Counter 原本会增加:
SUM(...) AS post_views
但 WordPress 的 fields=ids 又把 SELECT 强制收缩成:
wp_posts.ID
最终:
HAVING post_views
ORDER BY post_views
仍然存在,而 SELECT 中的 post_views 已经消失。
因此第一版方案被放弃。
十、MU Plugin 1.1.0:不再修改 fields=ids
第二版实现改变了策略。
不再告诉 WP_Query:
只返回 ID
而是让 Post Views Counter 按原来的方式构造:
- JOIN
- WHERE
- Polylang 语言过滤
- GROUP BY
- HAVING
- ORDER BY
最后仅通过 posts_fields 把 SELECT 列表收窄为:
wp_posts.ID,
wp_posts.post_date,
SUM(COALESCE(pvc.count, 0)) AS post_views
然后:
- 第一阶段得到已经排好顺序的 Top 10;
- 保存 ID 与
post_views; - 根据 Top 10 ID 获取完整
WP_Post; - 按第一阶段顺序重新组装;
- 把
post_views放回文章对象。
这样 Post Views Counter 后面的渲染逻辑基本无需改变。
当前 MU Plugin 版本:
1.1.0
十一、中文排行榜查询已经从 18 秒降低到 0.38 秒
启用 MU Plugin 1.1.0 后,再次直接调用:
pvc_get_most_viewed_posts()
结果:
elapsed=0.379339 seconds
count=10
更重要的是,排行榜数据完全一致。
前几名仍然是:
- 从 LetsVPN 停用至自建 WireGuard VPN 全流程复盘
- Telegram SMS Fee 与 Premium 短信验证码问题
- ZgoCloud + Wstunnel + WireGuard 提速实战
- WireGuard 国内直连 + 国外走隧道
- WireGuard VPN 国内直连优化
浏览量也完全一致。
说明优化没有牺牲数据正确性。
十二、中文动态首页:19 秒降到了约 0.84 秒
接下来重新绕过 EdgeOne 和 W3TC Page Cache,连续测试三次。
结果:
request=1 ttfb=0.887508 total=0.888629
request=2 ttfb=0.833429 total=0.834476
request=3 ttfb=0.835441 total=0.836537
优化前:
约 18.6~18.8 秒
优化后:
约 0.83~0.89 秒
差不多快了 22 倍。
而且这里测试的并不是 CDN HIT,也不是 W3TC Page Cache,而是真正执行 WordPress 的动态首页。
十三、英文首页也必须验证
我的中文首页和英文首页都放置了 Post Views Counter 热门文章排行榜。
目前也只有这两个首页使用这个排行榜。
所以只测试中文首页还不够。
首先直接测试英文排行榜:
elapsed=0.103985 seconds
count=10
返回的 10 篇全部都是英文文章,例如:
- Telegram SMS Fee 问题对应的英文文章
- Ubuntu 26.04 Fcitx5 输入法文章
- ZgoCloud / Wstunnel / Clash Verge Rev 开机启动文章
- WireGuard 排错文章
说明 Polylang 英文过滤没有被此次优化破坏。
十四、英文动态首页同样稳定在 1 秒以内
随后直接绕过 Cloudflare 和 W3TC Page Cache。
第一轮三次:
0.876757 秒
0.927064 秒
0.885427 秒
第二轮三次:
0.892663 秒
0.866815 秒
0.959465 秒
全部稳定在:
0.87~0.96 秒
同时在第二轮测试前记录 PHP-FPM slowlog 行数:
slowlog_before=37427
测试结束:
slowlog_after=37427
new_lines=0
也就是说:
连续三个真实英文动态首页请求,没有产生任何新的 PHP-FPM 慢日志。
当前 PHP-FPM 慢日志阈值是 3 秒,因此这个结果与首页不到 1 秒的测试数据完全吻合。
十五、slowlog 中还有记录,但已经与首页排行榜无关
测试完成以后,slowlog 中仍然能看到一些新记录。
但这些记录的调用栈变成了:
curl_exec()
WordPress HTTP API
WP AI Client
SlyTranslate
swq-glm52-translation-tuning.php
这是 SlyTranslate 调用 AI 接口时等待网络响应产生的慢请求。
它与 Post Views Counter 排行榜已经没有关系。
这其实也是一次很好的验证:
优化前,首页 slowlog 会明确停在:
pvc_get_most_viewed_posts()
优化后,这类调用已经消失。
剩余 slowlog 可以再按自己的业务场景单独分析,而不能继续把它归咎于首页排行榜。
十六、这个 MU Plugin 当前还有哪些限制?
目前这个 MU Plugin 是为了先解决生产环境中的真实问题,因此我刻意没有把修改范围扩大得太多。
最大的限制是:
当前只优化
Views period = 总浏览量(total)。
我的两个首页目前正好都是这种配置。
如果未来选择:
- 今日浏览量
- 本周浏览量
- 本月浏览量
- 某个时间范围
当前 MU Plugin 会继续使用 Post Views Counter 原始实现。
这不是因为两阶段查询只能处理 total,而是因为这些路径目前还没有逐一验证。
对于线上修复,我更倾向于:
已经确认的问题先精确解决,不把未经测试的行为顺手一起改掉。
十七、最终显示哪些字段,并不是这个方案最大的限制
排查过程中我还想到另一个问题。
Post Views Counter 的 Most Viewed Posts 区块可以选择显示:
- Post views
- Post excerpt
- Post author
- Post thumbnail
我目前首页只显示浏览量。
乍一看,似乎当前优化只适合我的配置。
但仔细分析后,其实并不是这样。
因为第二阶段最终获取的是:
完整的 WP_Post 对象
所以理论上即使以后显示:
- 摘要
- 作者
- 缩略图
这些数据仍然可以正常读取。
真正不应该做的,反而是因为最终需要显示摘要,就把 post_excerpt 加回第一阶段排行榜 SQL。
那样只会重新让大量文本字段参与临时表和排序。
因此更合理的架构仍然应该是:
排名所需字段最小化 → 得到 Top N → 再加载 Top N 的完整文章。
不过,因为我目前并没有逐项打开所有显示选项进行完整验证,所以现阶段仍然把它视为“理论兼容”,而不是已经完全测试过的功能。
十八、为什么暂时使用 MU Plugin,而不是直接修改插件?
目前没有直接修改:
wp-content/plugins/post-views-counter/
原因还是升级维护。
如果直接修改官方插件文件:
下一次 Post Views Counter 更新就很可能覆盖修改。
MU Plugin 则可以独立存在:
wp-content/mu-plugins/
出现问题也可以立即移除。
这对于一个生产站点上的临时性能修复更加安全。
当然,这不意味着我准备永久维护自己的分支。
我已经注册了 WordPress.org 账号,原本打算把这次问题和完整测试数据反馈给 Post Views Counter 作者。
不过目前账号仍然处于审核状态,所以暂时没有急着整理正式反馈。
等账号可以正常使用以后,再决定是否提交。
如果以后官方插件采用类似优化方案,我也更希望直接删除自己的 MU Plugin,重新回到官方实现。
十九、这次排查最值得记录的几个结论
这一次最大的收获,并不是简单地把某个页面从 19 秒优化到了不到 1 秒。
更重要的是几个排查思路。
1. CDN 很快,不代表 WordPress 本身很快
缓存 HIT 可能只有十几毫秒。
但真实动态页面可能正在后台跑 19 秒。
如果不主动绕过 CDN 和 Page Cache,这类问题非常容易长期隐藏。
2. 不要一看到 WordPress 慢就先增加服务器配置
这一次服务器:
- CPU 没有跑满
- 内存充足
- Swap 基本没用
- PHP-FPM worker 也够用
如果直接升级 ECS,可能只是用更多硬件掩盖一条错误的数据库查询。
3. EXPLAIN 使用了索引,也不代表 SQL 就一定快
这次相关表索引实际上都存在。
真正的问题是:
wp_posts.*
参与聚合排序以后,把包含 TEXT/LONGTEXT 的临时结果推到了磁盘。
4. 一条 SQL 不一定比两条 SQL 更快
原来的“一条完整查询”:
18.3 秒
拆成:
第一阶段排名:约 0.022 秒
第二阶段加载 Top 10:约 0.004 秒
反而快了几个数量级。
SQL 数量不是性能的唯一标准。
每条 SQL 到底让数据库做了什么,才更重要。
二十、结语
最开始遇到这个问题时,我确实怀疑过 PHP 8.5。
毕竟近期刚刚完成 PHP 版本升级,而且站点上也确实出现了一些 PHP 8.5 的 Deprecated 警告。
但这一次完整排查证明:
PHP 8.5 只是问题被发现时的背景,并不是动态首页接近 19 秒的真正原因。
真正的瓶颈是 Post Views Counter 热门文章查询:
完整 wp_posts.* + SUM + GROUP BY + ORDER BY
在 MySQL 5.7 上触发了非常昂贵的磁盘临时表。
最终通过:
轻量排名 + Top N 完整文章加载
把中文动态首页从约:
18.6~18.8 秒
降低到了:
0.83~0.89 秒
英文首页则稳定在:
0.87~0.96 秒
两个实际使用热门文章排行榜的首页都已经验证通过。
至少从目前结果来看,我已经没有必要为了这个问题升级服务器配置,也没有必要继续调整 PHP-FPM 参数。
这次真正需要优化的,不是机器,而是一条 SQL。
WordPress 网站维护、性能优化与博客运营咨询
本站已持续运营超过 10 年,累计发布 1000+ 篇原创技术文章,长期实践 WordPress 网站建设、CDN / Cloudflare 配置、缓存优化、Google SEO、广告变现和多语言网站运营。
如果你的 WordPress 网站遇到访问慢、缓存异常、插件冲突、广告不显示、SEO 基础结构混乱、CDN 配置不确定等问题,可以联系我做一次远程技术排查。
适合以下用户:
✅ 个人博客站长
✅ WordPress 网站运营者
✅ 独立开发者与内容创作者
✅ SaaS 产品官网运营团队
✅ 希望优化网站速度与稳定性的站点
服务内容:
✅ WordPress 速度优化
✅ Cloudflare / CDN / 缓存配置排查
✅ 插件冲突与页面异常排查
✅ AdSense 广告显示问题排查
✅ SEO 基础结构检查
✅ 博客运营与商业化咨询
如需了解方案或交流相关问题,请直接联系我,并注明:WordPress 维护咨询。
联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com


发表回复