WordPress サイト(AlmaLinux + Apache 2.4 + PHP-FPM + MariaDB、WPは /wpm 配下)で、ページキャッシュはあるのに TTFB が ~0.6s のままという状態を、mod_cache_disk による静的HTML直配信で ~12ms まで短縮した実録メモです。
そのままコピペできる設定・確認コマンド・運用小ネタまで一気にどうぞ。
まず結論(成果)
- 変更前(ホーム): TTFB ≈ 0.60–0.63s
- 変更後(mod_cache_disk HIT): TTFB ≈ 0.012s(12ms)
- レスポンスヘッダ(例)
x-cache: HIT from hd0.biz
age: 185
cache-control: public, max-age=1800, s-maxage=1800, stale-while-revalidate=120, stale-if-error=86400
方針
- HTMLに十分なTTLを付ける(
max-ageを数十秒〜数十分に) - WordPressやテーマが勝手に付ける短命ヘッダを上書き/限定
- Apacheの mod_cache_disk で匿名HTMLをディスクキャッシュし、次回からはPHPを通さず配信
- 確認と運用(ウォームアップ・掃除・デバッグ)
ステップ1:HTMLのTTLを“確実に”上書き
vhost(*:443)に入れたブロック(抜粋)
ポイント:append ではなく always set、先に unset。
ログイン/管理/API だけは no-store にします。
# ================= キャッシュ制御(HTMLを強キャッシュ) ================= # 管理/ログイン/API はキャッシュさせない判定
SetEnvIfNoCase Request_URI "^/(wp-admin|wp-login\.php|xmlrpc\.php|wp-json)" nocache=1
SetEnvIfNoCase Cookie "(wordpress_logged_in|comment_author|woocommerce_)=" loggedin=1 <IfModule mod_headers.c> # 既存の短命ヘッダを打ち消す Header always unset Cache-Control Header always unset Expires # 匿名HTMLは30分キャッシュ+stale配信 Header always set Cache-Control "public, max-age=1800, s-maxage=1800, stale-while-revalidate=120, stale-if-error=86400" env=!nocache # ログイン/管理/API は非キャッシュ Header always set Cache-Control "private, no-store" env=loggedin Header always set Cache-Control "private, no-store" env=nocache # 圧縮分岐の明示 Header append Vary "Accept-Encoding"
</IfModule>
(補足).htaccess 側は静的拡張子だけに限定
HTMLに触らないよう FilesMatch で静的ファイルに限定します。
/var/www/html/.htaccess 例:
<IfModule mod_headers.c> <FilesMatch "\.(?:css|js|mjs|svg|json|txt|woff2?)$"> Header set Cache-Control "max-age=31536000, public" </FilesMatch> Header append Vary Accept-Encoding env=!dont-vary
</IfModule>
/wpm/wp-content/.htaccess(画像の private を撤去→public に)
<IfModule mod_headers.c> <FilesMatch "(?i)\.(jpe?g|png|gif|svg|webp|avif)$"> Header always set Cache-Control "max-age=31536000, public" Header append Vary "Accept" # WebP/AVIF のネゴシ用 </FilesMatch>
</IfModule>
ステップ2:WordPress側が出す“1秒ヘッダ”を抑える(MUプラグイン)
WPやプラグインが HTML に短命ヘッダを付けても、最後に上書きします。
/wpm/wp-content/mu-plugins/cache-headers.php:
<?php
/*
Plugin Name: Cache Headers (HTML)
Description: Anonymous HTML に長めの Cache-Control を付与
*/ add_action('send_headers', function () { if ( is_user_logged_in() || is_preview() || is_admin() ) { header_remove('Cache-Control'); header('Cache-Control: private, no-store'); return; } header_remove('Cache-Control'); header('Cache-Control: public, max-age=1800, s-maxage=1800, stale-while-revalidate=120, stale-if-error=86400');
}, 9999);
ステップ3:mod_cache_disk で“静的HTML直配信”
Vary付きドキュメントも保存するのがコツ(
CacheNegotiatedDocs On)。
さらにCacheIgnoreHeaders Set-Cookieで“ゆるいCookie”があっても匿名ページを弾かないようにします。
vhost(*:443)に追記:
# ==== mod_cache_disk で匿名HTMLをディスクキャッシュ ====
CacheRoot /var/cache/httpd/mod_cache_disk # 交渉ドキュメントもキャッシュ対象に
CacheNegotiatedDocs On CacheQuickHandler On
CacheLock On
CacheIgnoreQueryString Off
CacheStorePrivate Off
CacheStoreNoStore Off
CacheMaxFileSize 10485760
CacheMaxExpire 1800
CacheDefaultExpire 1800 # Set-Cookie はキャッシュ判断から除外(匿名ページを弾かない)
CacheIgnoreHeaders Set-Cookie # デバッグ(任意):X-Cache / X-Cache-Detail が付く
CacheHeader On # 管理/ログイン/API はキャッシュ無効
CacheDisable /wp-admin
CacheDisable /wp-login.php
CacheDisable /xmlrpc.php
CacheDisable /wp-json # UTM等は無視してHIT率UP(任意)
CacheIgnoreURLSessionIdentifiers sid PHPSESSID ASPSESSIONID utm_source utm_medium utm_campaign utm_term utm_content gclid fbclid # Cookieがあるときは回避フラグ
SetEnvIfNoCase Cookie "(wordpress_logged_in|comment_author|woocommerce_)" cache_bypass=1 # 匿名時は Set-Cookie を出力から除去してキャッシュ可能化
Header always unset Set-Cookie env=!cache_bypass # ルート配下をキャッシュ対象に
CacheEnable disk / # バイパス時は非キャッシュ(保険)
Header always set Cache-Control "private, no-store" env=cache_bypass
初回はMISS → 2回目以降HIT
保存後は PHPを通らずディスクからHTMLを直配信するため、TTFB が劇的に短縮します。
ステップ4:確認コマンド(そのままコピペ)
4.1 HTMLヘッダ(TTL)
curl -sI https://example.com/ | egrep -i 'cache-control|age|x-cache|strict-transport'
期待:
cache-control: public, max-age=1800, s-maxage=1800, stale-while-revalidate=120, stale-if-error=86400
x-cache: HIT from ...
age: 10(数秒待って再取得)
4.2 実測(TTFB)
for i in 1 2 3; do curl -o /dev/null -s -w 'ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/;
done
期待:HIT時 0.30〜0.45s(環境次第)。今回の事例では ~0.012s まで短縮。
4.3 画像ヘッダ(1年キャッシュ)
curl -sI https://example.com/wpm/wp-content/uploads/2022/09/DSC_0255-100x100.jpg | egrep -i 'cache-control|vary|expires'
# => Cache-Control: max-age=31536000, public
ステップ5:運用Tips(任意)
5.1 キャッシュ掃除(htcacheclean)
- 手動一回:
sudo htcacheclean -p /var/cache/httpd/mod_cache_disk -l 2G -t -v
15分ごとに自動掃除(systemd timer):
# /etc/systemd/system/mod_cache_disk-clean.service
[Unit]
Description=Clean Apache mod_cache_disk
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/htcacheclean -p /var/cache/httpd/mod_cache_disk -l 2G -n -t # /etc/systemd/system/mod_cache_disk-clean.timer
[Unit]
Description=Clean Apache mod_cache_disk every 15 minutes
[Timer]
OnUnitActiveSec=15m
Unit=mod_cache_disk-clean.service
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now mod_cache_disk-clean.timer
5.2 ウォームアップ(ホームを10分ごとに温める)
# /etc/systemd/system/cache-warm.service
[Unit]
Description=Warm Apache mod_cache_disk (homepage)
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/curl -sS https://example.com/ >/dev/null # /etc/systemd/system/cache-warm.timer
[Unit]
Description=Warm Apache mod_cache_disk every 10 minutes
[Timer]
OnUnitActiveSec=10m
Unit=cache-warm.service
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now cache-warm.timer
5.3 デバッグ時だけ有効にするログ(原因特定が一撃)
# vhostに一時的に
LogLevel warn cache:debug cache_disk:debug headers:trace1
hd0_ssl_error.logに「not cacheable because …」が出て、弾かれる理由(Cookie/ヘッダ/認証など)が一発で分かります。調査後は元に戻しましょう。
よくあるハマりどころ
Vary: Accept-Encodingが付くと保存されない
→CacheNegotiatedDocs Onを必ず入れる。- 匿名でも
Set-Cookieが先に出る
→CacheIgnoreHeaders Set-Cookie+ 出力時にHeader always unset Set-Cookie env=!cache_bypass。 - どこかで
Header append Cache-Control "public"
→ ルートやテーマの .htaccess を静的拡張子限定に。HTMLを触らない。 - Age が 0 のまま
→ 直後は普通。2秒ほど待って再取得。 AH01071: Primary script unknown
→ ないPHPを叩くボットのノイズ。実害なし。
さらに速く・堅くしたい場合
- PHP-FPM のワーカー調整(
pm.max_childrenほか)+ OPcache強化 - Redis Object Cache でDB往復を削減(WPプラグイン+
wp-config.phpだけでOK) - Cloudflare APO / Cache Everything を併用してエッジ配信(海外アクセスやTLS終端の短縮に有効)
まとめ
- HTMLに適切なTTL(30分+stale)を付け、
- .htaccessを静的限定に整理し、
- mod_cache_disk による匿名HTMLのディスクHITを作れば、
ページキャッシュは効いているのに TTFB が遅い問題は、劇的に改善できます。
実測では ~0.6s → ~0.012s。
仕組みが合えば、まだまだ“配信側”で伸びしろはあります。

