自分が運営しているWordPressサイトの表示が遅くなっていました。Lighthouseで計測すると、パフォーマンススコアが50を下回ることもあります。原因を探ろうとしたものの、どの処理が重いのか特定できず困っていました。
そこで、リクエストごとの処理時間とデータベース負荷を記録するプラグインを作ることにしました。
1. なぜLighthouseだけでは足りないのか
最初はGoogle Lighthouseを使っていました。スコアは出るのですが、「何が遅いか」までは分かりません1。ブラウザから見た結果しか得られないため、サーバー側のどの処理に時間がかかっているのかが見えないのです。
WordPressはPHPで動いています。リクエストを受け取ってから、プラグインの初期化、テーマの読み込み、データベースへの問い合わせ、HTML生成と、いくつもの段階を経てページを返します。この内部の流れを可視化する必要がありました。
外から見た結果と、内部の処理時間の両方があれば、ボトルネックを具体的に特定できます。
1.1. 計測プラグインの基本設計
WordPressには、処理の各段階で実行されるフック(hook)という仕組みがあります2。プラグインの読み込み時にはplugins_loaded、初期化時にはinit、ヘッダー出力時にはwp_headといった具合です。
これらのフックに処理を登録しておけば、通過した時刻を記録できます。時刻の差分を取れば、各区間にかかった時間が分かるわけです。
class Chiilabo_Perf_Profiler {
private $boot_time;
private $marks = array();
public function init() {
$this->boot_time = microtime(true);
add_action('plugins_loaded', array($this, 'mark_plugins_loaded'));
add_action('init', array($this, 'mark_init'));
add_action('wp', array($this, 'mark_wp'));
add_action('template_redirect', array($this, 'mark_template_redirect'));
add_action('wp_head', array($this, 'mark_wp_head'));
add_action('wp_footer', array($this, 'mark_wp_footer'));
add_action('shutdown', array($this, 'shutdown'));
}
public function mark_plugins_loaded() {
$this->marks['plugins_loaded'] = microtime(true);
}
}
Code language: PHP (php)
microtime(true)で現在時刻をマイクロ秒単位で取得し、配列に保存しています3。最後のshutdownフックで全データを集約してログに書き出す流れです。
1.2. プラグインの構造
現在のプラグインは単一ファイル構成です。責務を「計測」「出力」「設定」「管理操作」に分けつつ、クラス内のメソッドとして実装しています。
将来的には分割が必要かもしれません。計測ロジックをRequestProfiler、ログ出力をLogWriter、設定画面をAdminSettingsのように分けると保守しやすくなります。
ただし、現段階ではプロトタイプとして、シンプルさを優先しました。後方互換やフォールバックを減らし、管理画面からの操作に寄せることで、動作が安定しています。
1.3. 記録する情報を決める
時間だけでなく、データベースへの問い合わせ回数も重要です。WordPressはSAVEQUERIESという定数をtrueにすると、実行されたSQLクエリを記録してくれます4。
if (defined('SAVEQUERIES') && SAVEQUERIES && isset($GLOBALS['wpdb']->queries)) {
$queries = $GLOBALS['wpdb']->queries;
$db_query_count = count($queries);
$db_query_time_ms = 0;
foreach ($queries as $q) {
$db_query_time_ms += $q[1] * 1000;
}
}
Code language: PHP (php)
これで何回クエリが実行され、合計で何ミリ秒かかったかが分かります。
記録する項目は次のように決めました。
- リクエスト情報: タイムスタンプ、ホスト名、URI、HTTPメソッド、ステータスコード
- 時間情報: 総処理時間、各区間の処理時間
- データベース: クエリ回数、クエリ実行時間
- 環境情報: 使用メモリ、有効なプラグイン数、テーマ名
- 分析補助: 識別用コメント、User-Agent、Referer
これらをJSON Lines形式(1行1JSONのテキストファイル)で保存します5。
{"timestamp":"2026-02-15T10:30:45+09:00","host":"example.com","uri":"/article/sample","method":"GET","status":200,"total_ms":245.3,"segment_ms":{"boot_to_plugins_loaded":12.4,"plugins_loaded_to_init":34.2,"init_to_wp":15.6,"wp_to_template_redirect":8.1,"template_redirect_to_wp_head":45.8,"wp_head_to_wp_footer":118.5,"wp_footer_to_shutdown":10.7},"db_query_count":42,"db_query_time_ms":89.2,"memory_peak_mb":45.6,"run_comment":"baseline"}
Code language: JSON / JSON with Comments (json)
1リクエストが1行なので、後からスクリプトで集計しやすくなります。
1.4. 管理画面での操作を簡単にする
ログファイルはWordPressのwp-content/uploads/chiilabo-perf/に保存されます6。FTPで取り出すこともできますが、手間がかかります。管理画面から直接ダウンロードできるようにしました。
public function render_settings_page() {
?>
<div class="wrap">
<h1>Chiilabo 計測プロファイラ</h1>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<input type="hidden" name="action" value="chiilabo_perf_download_log">
<?php wp_nonce_field('chiilabo_perf_download_log'); ?>
<p>
<label>ファイル名に含めるコメント(任意):</label><br>
<input type="text" name="log_comment" value="" size="40">
</p>
<button type="submit" class="button">requests.jsonl をダウンロード</button>
</form>
</div>
<?php
}
Code language: HTML, XML (xml)
ダウンロード時にコメントを入力できるようにしました。これでファイル名がrequests-20260215-103045-baseline.jsonlのように区別しやすくなります。
クリア機能も付けました。クリアする前に、入力されたコメント付きで自動的にバックアップを作成します7。誤って消してしまう心配が減りました。
2. 比較実験で見えてきた問題
このプラグインを使って、機能のON/OFF比較を始めました8。自分のサイトはCocoonというテーマを使っており、functions.phpでいくつか機能を追加していました。
比較するには、条件を識別する必要があります。そこでrun_commentという項目を追加しました。管理画面で「off」と入力してアクセステストを実行し、次に「on」と入力して同じテストを実行します。ログには条件が記録されるので、後から抽出できます。
# 機能OFF状態でテスト
curl -s "https://example.com/article/sample" > /dev/null
curl -s "https://example.com/category/tech" > /dev/null
# 機能ON状態でテスト
curl -s "https://example.com/article/sample" > /dev/null
curl -s "https://example.com/category/tech" > /dev/null
Code language: PHP (php)
ログからrun_commentが「off」の行と「on」の行を抽出し、同じURIで平均処理時間を比較しました。
結果は予想外でした。backlinkという機能をOFFにしたほうが、全体平均で速くなっていたのです。有効にしていた機能が、逆に遅くしていました。
2.1. 区間ごとの分析で分かったこと
総処理時間だけでなく、区間ごとの時間も記録していたのが役立ちました。遅いページの多くで、wp_head_to_wp_footerという区間が支配的だったのです。
この区間は、ヘッダーを出力してからフッターを出力するまで、つまりページの本文を生成している部分です。ショートコード(WordPressの記法で、記事内に特定の機能を埋め込むもの)の処理や、本文の変換処理がここに含まれます。
functions.phpで追加していた処理の多くは、本文に対するフィルタでした。the_contentというフィルタにフック登録し、本文を書き換えています9。複数のフィルタが順番に実行されるため、同じ本文に何度も正規表現を適用していました10。
add_filter('the_content', function($content) {
$content = str_replace('旧表記', '新表記', $content);
return $content;
}, 10);
add_filter('the_content', function($content) {
$content = preg_replace_callback('/パターン/', function($m) {
return '置換結果';
}, $content);
return $content;
}, 20);
Code language: PHP (php)
このような処理が積み上がっていたわけです。改善するには、実行範囲を限定するか、保存時に前処理しておく必要があります。
2.2. 予想外の発見: /tag//問題
ログを眺めていると、/tag//というURIが頻繁に記録されていました。タグアーカイブのURLですが、スラッグ部分が空になっています。テーマの内部リンク生成にバグがあるのかと思いました。
ここで、User-AgentとRefererを記録するように改良しました。
private function collect_request_meta() {
return array(
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
);
}
Code language: PHP (php)
結果を見ると、User-Agentの上位はbingbotやSogou spiderといったクローラーでした11。Refererは空が大半です。つまり、外部のクローラーが直接アクセスしていたのです。
サイト内のリンクが原因ではなく、検索エンジンのクローラーが過去のインデックスや推測で不正なURLを叩いていました。テーマを修正する前に、クローラー対策や正規化処理を優先すべきだと分かりました。
3. 運用で気をつけたこと
計測を続けるうちに、いくつか運用上の注意点が見えてきました。
3.1. ログの分離
同一コマンドでアクセステストを実行しても、requests.jsonlに通常アクセスが混ざると比較精度が落ちます12。各条件の直後にログを回収し、別ファイルとして保存する運用が必須でした。
管理画面でダウンロード時にコメントを入力できるようにしたのは、この運用を楽にするためです。
3.2. 再現条件の固定
比較実験では、URLセットや計測窓を揃えないと判断がぶれます。アクセステストのスクリプトに、ウォームアップ(事前アクセス)とラベル(条件識別)の仕組みを追加しました。
# ウォームアップ
for url in "${URLS[@]}"; do
curl -s "$url" > /dev/null
done
# 本計測(run_commentを設定した状態で実行)
for url in "${URLS[@]}"; do
curl -s "$url" > /dev/null
done
Code language: PHP (php)
キャッシュの影響を安定させるため、本計測前に一度アクセスしておきます13。
3.3. テスト先行の実装
管理画面のボタンを追加する際、実装前に失敗するテストを書きました。
public function test_download_button_is_disabled_when_no_log_file() {
// ログファイルが存在しない状態を作る
// ボタンが無効化されているか確認
}
Code language: PHP (php)
これにより、ボタンの無効化処理を確実に実装できました。後から不具合を見つけて直すより、最初から正しく作れます。
4. 分かったこと、残った課題
このプラグインを使って、いくつかの発見がありました。
- backlink機能をOFFにしたほうが速い傾向が複数回観測された
/tag//の頻出は外部クローラーが原因で、テーマのバグではなかったwp_head_to_wp_footer区間に遅延が集中しやすい
一方で、課題も残っています。
比較実験の抽出処理は、まだスクリプト運用に依存しています。管理画面で条件を指定して集計結果を表示できれば、もっと使いやすくなるでしょう。
また、run_commentとダウンロード時のlog_commentは役割が違います。前者は計測条件の識別で、後者はファイル名の区別です。この違いを説明するドキュメントが必要です。
- Lighthouseはブラウザから見たパフォーマンスを計測するツールで、First Contentful Paint (FCP)やLargest Contentful Paint (LCP)などのCore Web Vitalsを測定できます。しかし、サーバー側の処理内訳は取得できません。 – PageSpeed Insights
- WordPressのフック実行順序の詳細については、公式ドキュメントを参照してください。フロントエンド表示では、plugins_loaded → setup_theme → after_setup_theme → init → wp_loaded → wp → template_redirect → wp_head → wp_footer → shutdownの順で実行されます。 – Action Reference – WordPress Developer Resources
- より正確なパフォーマンス計測が必要な場合、PHP 7.3以降では
hrtime()関数の使用が推奨されています。ただし、WordPressの幅広い環境互換性を考慮するとmicrotime(true)で十分です。また、リクエスト全体の処理時間を測る場合は$_SERVER['REQUEST_TIME_FLOAT']を開始時刻として使用するとより正確です。 – PHP: microtime – Manual - SAVEQUERIESを有効にするには、wp-config.phpファイルに
define('SAVEQUERIES', true);を追記します。ただし、この機能はメモリ使用量とCPU負荷を増加させるため、本番環境では必ず無効化し、開発環境またはステージング環境でのみ使用してください。 – Save Database Queries for Analysis in WordPress - JSON Lines (JSONL)は、1行に1つの完全なJSONオブジェクトを記録する形式で、ログファイルやストリーミングデータに適しています。ファイル拡張子は.jsonlが推奨されます。各行は独立したJSONオブジェクトであり、ファイル全体をJSON配列として解析する必要がないため、大量のログを効率的に処理できます。 – JSON Lines
- wp-content/uploads/配下のファイルは通常、Webブラウザから直接アクセスできます。ログに機密情報が含まれる可能性がある場合は、.htaccessでアクセス制限をかけるか、wp-content直下など別の場所に保存することを検討してください。 – WordPress Security Best Practices
- ログファイルは継続的に追記されるため、放置すると肥大化します。本番運用では定期的なクリアやログローテーションの仕組みが必要です。Linuxのlogrotateのような機能を実装するか、一定サイズを超えたら古いログをアーカイブする仕組みを検討してください。
- WordPressには既にQuery Monitorという優れたパフォーマンス分析プラグインが存在します。しかし、Query Monitorはリクエストごとの分析に特化しており、継続的な自動計測やON/OFF比較のための条件識別機能は提供していません。この記事のプラグインは、長期的なパフォーマンストレンド分析と比較実験に重点を置いています。 – Query Monitor Plugin
- the_contentフィルタは、投稿本文が表示される直前に実行されます。フィルタの第2引数に優先度(priority)を指定でき、数値が小さいほど早く実行されます。デフォルトは10です。複数のフィルタが登録されている場合、優先度の順に実行されます。 – Plugin API/Filter Reference – WordPress Codex
- WordPressのパフォーマンス最適化では、N+1クエリ問題(ループ内でのデータベースクエリ実行)が最大のボトルネックになることが多いです。メタデータの取得は
update_postmeta_cache()で事前にキャッシュする、トランジェントAPIでクエリ結果をキャッシュするなどの対策が有効です。 – WordPress Performance Optimization - クローラーは検索エンジンがウェブページを巡回して情報を収集する自動プログラムです。Google、Bing、Baiduなど各検索エンジンが独自のクローラーを運用しています。robots.txtファイルやmetaタグでクローラーの動作を制御できます。 – Robots.txt Specifications
- ログファイルへの同時書き込みは、PHPのfile_put_contents()に
FILE_APPEND | LOCK_EXフラグを指定することで排他制御できます。LOCK_EXは書き込み時にファイルロックを取得し、他のプロセスが同時に書き込むのを防ぎます。 – PHP: file_put_contents – Manual - WordPressのパフォーマンス計測では、オブジェクトキャッシュ(Redis、Memcached)、OPcache(PHPバイトコードキャッシュ)、ページキャッシュ(WP Rocket、LiteSpeed Cache)など複数のキャッシュ層が影響します。公平な比較のためには、各層のキャッシュ状態を揃える必要があります。 – WordPress Caching Best Practices