前言
WordPress 慢是刻板印象。慢的原因是没做缓存层。一旦叠满缓存,请求进 nginx 就直接返回,根本到不了 PHP——和 Hexo / Hugo 这类静态站走的是同一条路径。
本文给一份完整可用的 nginx 配置 + 主题侧清理钩子接入。目标:保留后台发文、评论、用户系统这些动态能力,首屏 TTFB 压到 10ms 以内。
缓存层叠满后的请求链路
没缓存的 WordPress:
请求 → nginx → PHP-FPM → WordPress 启动 → 插件加载 → 主题加载 → 数据库查询 → 模板渲染 → 返回
叠满缓存之后:
请求 → nginx 拿到缓存 → 返回
等价于 nginx 直接吐 HTML 文件。各层缓存职责:
- Redis / Memcached Object Cache:缓存数据库查询
- 主题层整页缓存:把 HTML 渲染结果存成文件,PHP 启动早期 die 返回
- OPcache:PHP 脚本编译成字节码,省重复编译
- Nginx FastCGI Cache:在 nginx 阶段短路掉 PHP-FPM 调用,请求根本不进 PHP
前三层是"减少 PHP 内部开销",FastCGI Cache 是"压根不进 PHP",这是最后一公里。
第一步:建目录并赋权
缓存目录选自己 WordPress 站点里的路径。推荐放在 wp-content/cache/ 下,和主题整页缓存同目录层,权限统一好管理。
把下面路径里的 /www/wwwroot/你的站点目录/ 换成自己站点实际根目录:
mkdir -p /www/wwwroot/你的站点目录/wp-content/cache/nginx
chown www:www /www/wwwroot/你的站点目录/wp-content/cache/nginx
chmod 755 /www/wwwroot/你的站点目录/wp-content/cache/nginx
nginx worker 以 www 用户运行,需要读写权限。如果是其他面板或自定义环境,把属主换成 nginx worker 实际运行的用户。
第二步:nginx.conf 定义缓存区
在 http 块尾部加全局配置。这一段属于整个 nginx,不归属单个站点。
同样把 /www/wwwroot/你的站点目录/ 换成自己的站点根目录:
# ============ FastCGI Cache ============
fastcgi_cache_path /www/wwwroot/你的站点目录/wp-content/cache/nginx
levels=1:2
keys_zone=WORDPRESS_CACHE:100m
max_size=500m
inactive=1d
use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_lock on;
fastcgi_cache_lock_age 5s;
# ============ FastCGI Cache END ============
参数解释:
keys_zone=WORDPRESS_CACHE:100m:共享内存存缓存键元数据,100MB 能容纳几十万个 URL,单博客够用max_size=500m:磁盘缓存总上限,超了按 LRU 淘汰inactive=1d:1 天没访问就清掉levels=1:2:目录层级,避免单目录文件过多use_temp_path=off:直接写最终目录,省一次跨设备拷贝
第三步:站点 server 块配置
宝塔默认站点配置里 PHP 是通过 include enable-php-83.conf; 引用的,但 fastcgi_cache 指令必须和 fastcgi_pass 在同一个 location 块里,没法通过 include 注入。所以要把 PHP 段内联出来。
完整 server 块(SSL、HTTPS 跳转、伪静态这些省略,只看 fastcgi cache 相关):
server {
listen 443 ssl;
server_name your-domain.com;
root /www/wwwroot/你的站点目录;
# ============ 1. 缓存闸门 ============
# 默认跳过缓存
set $skip_cache 1;
# 仅当原始请求 URI 为 "/" 时才允许缓存(首页)
if ($request_uri = "/") {
set $skip_cache 0;
}
# 仅 GET/HEAD 才缓存
if ($request_method !~ ^(GET|HEAD)$) {
set $skip_cache 1;
}
# 登录用户 / 评论作者 / 受密码保护文章一律跳过
if ($http_cookie ~* "wordpress_logged_in|comment_author|wp-postpass") {
set $skip_cache 1;
}
# 暴露缓存命中状态,便于调试
add_header X-Cache-Status $upstream_cache_status;
# ============ 缓存闸门 END ============
# ============ 2. 禁止 web 访问缓存目录 ============
location ^~ /wp-content/cache/nginx/ {
deny all;
return 403;
}
# ============ 3. PHP 段内联 + fastcgi cache ============
location ~ [^/]\.php(/|$) {
try_files $uri =404;
fastcgi_pass unix:/tmp/php-cgi-83.sock;
fastcgi_index index.php;
include fastcgi.conf;
include pathinfo.conf;
# 缓存指令
fastcgi_cache WORDPRESS_CACHE;
fastcgi_cache_valid 200 301 302 1d;
fastcgi_cache_valid 404 1m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_lock on;
fastcgi_cache_lock_age 5s;
# 必须保留:忽略 PHP 输出的 Cache-Control / Expires / Set-Cookie
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
# 透传主题自定义响应头
fastcgi_pass_header Cache-Control;
fastcgi_pass_header Pragma;
fastcgi_pass_header Expires;
fastcgi_pass_header ETag;
fastcgi_pass_header Last-Modified;
fastcgi_pass_header Vary;
fastcgi_pass_header Content-Length;
fastcgi_pass_header X-Cache-Type;
}
}
配置拆解
1. $skip_cache 闸门:决定"要不要缓存"
闸门判断三件事:
- URI 必须是
/(首页)——本教程只缓存首页的核心规则 - 必须是 GET 或 HEAD——POST 请求(评论提交、登录)一律放过
- 不能带
wordpress_logged_in/comment_author/wp-postpasscookie——登录态、评论作者、受密码保护文章不缓存
这套闸门不只是防漏缓存,更防"登录态被缓存进首页、所有访客共享同一份缓存"这种灾难。
2. fastcgi_ignore_headers:决定"nginx 是否写入缓存"
WordPress 和大部分主题的 PHP 都会发 Cache-Control 头,类似:
Cache-Control: public, max-age=600, s-maxage=600, stale-while-revalidate=600, stale-if-error=600
nginx 拿到带 s-maxage 和 stale-while-revalidate 的头,会严格判断"这玩意只能被 CDN 缓存、不能被 FastCGI Cache 缓存",直接拒绝写入。
我配置时把这行删了测试,连续 5 次首页访问全部 MISS,没有一次 HIT。加回来立刻正常。缓存行为由 fastcgi_cache_valid 决定,不再受 PHP 头影响。
3. fastcgi_pass_header:决定"主题头是否透传"
fastcgi 默认不透传 X-* 自定义头。主题用来标记缓存状态的头(X-Cache-Type / X-Pix-Page-Cache 之类)需要显式 fastcgi_pass_header 才能在浏览器 DevTools 看到。响应头一字不差地透传是验证缓存命中正确性的关键。
第四步:主题侧清理机制对接
FastCGI Cache 弱点是 nginx 原生不支持按 URL 自动清理。首页内容更新后想立即看到,得有人主动清缓存。最顺手的办法是借用主题里"清除首页缓存"按钮的处理函数。
大部分主题都有一套自己的整页缓存清理逻辑,找到那个函数,在末尾追加一段递归删除 nginx 缓存目录的代码:
function clear_theme_page_cache() {
// ... 主题原本的清理逻辑 ...
// 同步清除 Nginx FastCGI Cache
$cache_dir = WP_CONTENT_DIR . '/cache/nginx';
if (!is_dir($cache_dir)) {
return;
}
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($cache_dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir()) {
@rmdir($file->getRealPath());
} else {
@unlink($file->getRealPath());
}
}
} catch (Exception $e) {
// 静默失败,避免影响主题清理流程
}
}
路径用 WP_CONTENT_DIR 常量动态拼接,不写死绝对路径,迁移站点时不用改代码。
主题里所有调用清理函数的地方(清理按钮、关闭缓存开关、文章清理钩子)都会顺带清 nginx 缓存,不需要再单独加 transition_post_status / save_post 钩子,主题自身的清理流程已经覆盖这些场景。
踩坑记录
坑 1:duplicate 错误
报错:
nginx: [emerg] "fastcgi_cache_key" directive is duplicate in /www/server/panel/vhost/nginx/0.fastcgi_cache.conf:1
宝塔面板的"Nginx 缓存"插件会自动生成 0.fastcgi_cache.conf,里面已经定义了一组 fastcgi_cache 全局指令,和手动加的重复。
解决:在宝塔面板关掉那个开关,或直接改名备份:
mv /www/server/panel/vhost/nginx/0.fastcgi_cache.conf /www/server/panel/vhost/nginx/0.fastcgi_cache.conf.bak
坑 2:连续 MISS 不 HIT
5 次首页全部 MISS。原因就是上面说的 fastcgi_ignore_headers 被删了,主题输出 s-maxage 后 nginx 拒绝写入。加回来就好。
主题早期缓存和 FastCGI Cache 的关系
主题早期缓存(比如 HTTP 头里 X-Pix-Page-Cache: EARLY-HIT)在 PHP 启动早期 die 返回,和 FastCGI Cache 不冲突。两层叠加:
- FastCGI Cache 命中:请求根本不到 PHP,早期缓存没机会跑
- FastCGI Cache 未命中:进 PHP,早期缓存命中就 die,响应头里两个标记共存
扩展:从首页缓存到全站缓存
默认配置只缓存首页——单用户博客最常见的需求,游客为主,发文靠手动清理按钮。
想全站缓存:删掉 if ($request_uri = "/") { set $skip_cache 0; } 这段,闸门只剩 GET/HEAD 和登录态检查,所有 GET 请求都缓存。
想白名单缓存:把那个 if 改成 URI 正则:
if ($request_uri ~ "^/$|^/articles/|^/tags/") {
set $skip_cache 0;
}
POST、登录态、带 query string 的请求自动 bypass,不用额外配置。
验证
# 1. 测试配置并重载
nginx -t
nginx -s reload
# 2. 清掉旧缓存
rm -rf /www/wwwroot/你的站点目录/wp-content/cache/nginx/*
# 3. 连续打 5 次首页
curl -I https://your-domain.com/ # MISS
curl -I https://your-domain.com/ # HIT
curl -I https://your-domain.com/ # HIT
curl -I https://your-domain.com/ # HIT
curl -I https://your-domain.com/ # HIT
预期:第 1 次 MISS,后 4 次 HIT。HIT 时响应头的 Cache-Control / ETag / Last-Modified / Vary 必须和 MISS 时完全一致,否则就是有头被 fastcgi 吞了。
其他场景必须 BYPASS:
- 文章页:
BYPASS - 带 query string(
/?paged=2):BYPASS - 带
wordpress_logged_incookie:BYPASS - 直接访问缓存目录(
/wp-content/cache/nginx/):403
调优
fastcgi_cache_valid 200 301 302 1d;:1 天有效期,发文靠主题按钮清理兜底max_size=500m:大概能存 200+ 个首页版本(单 HTML 50-80KB),单博客够用open_file_cache max=1000 inactive=20s;:在 http 块加这一行,让 nginx 缓存文件元数据,每次读盘少一次 stat 系统调用,TTFB 再压 1-2msfastcgi_cache_use_stale error timeout ...:后端出错时返回旧缓存而不是 502,PHP-FPM 偶尔卡一下也不影响访客
不上 CDN,TTFB 稳定 20ms 以内,运气好 5-10ms。Hexo / Hugo 同档的速度,WordPress 的灵活度。