自分のWordPressサイトが
重い原因をプラグインで特定した
(Chiilabo Perf Profiler)

自分が運営しているWordPressサイトの表示が遅くなっていました。Lighthouseで計測すると、パフォーマンススコアが50を下回ることもあります。原因を探ろうとしたものの、どの処理が重いのか特定できず困っていました。

そこで、リクエストごとの処理時間とデータベース負荷を記録するプラグインを作ることにしました。

関連記事

1. なぜLighthouseだけでは足りないのか

なぜLighthouseだけでは足りないのか Lighthouse スコアは出る サーバー内部 何が遅いか 見えない 内部計測で可視化が必要 プラグイン初期化 DB問い合わせ HTML生成

最初はGoogle Lighthouseを使っていました。スコアは出るのですが、「何が遅いか」までは分かりません1。ブラウザから見た結果しか得られないため、サーバー側のどの処理に時間がかかっているのかが見えないのです。

WordPressはPHPで動いています。リクエストを受け取ってから、プラグインの初期化、テーマの読み込み、データベースへの問い合わせ、HTML生成と、いくつもの段階を経てページを返します。この内部の流れを可視化する必要がありました。

外から見た結果と、内部の処理時間の両方があれば、ボトルネックを具体的に特定できます。

1.1. 計測プラグインの基本設計

WordPressには、処理の各段階で実行されるフック(hook)という仕組みがあります2。プラグインの読み込み時にはplugins_loaded、初期化時にはinit、ヘッダー出力時にはwp_headといった具合です。

計測プラグインの基本設計 WordPressフックで時刻記録 plugins_loaded → 時刻記録 init → 時刻記録 wp_head → 時刻記録 shutdown → ログ書き出し 時刻差

これらのフックに処理を登録しておけば、通過した時刻を記録できます。時刻の差分を取れば、各区間にかかった時間が分かるわけです。

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

記録する情報を決める JSON Lines形式で保存 1行 = 1リクエスト 時間情報 総処理時間 区間時間 DB情報 クエリ回数 実行時間 リクエスト情報 URI / メソッド 分析補助 User-Agent Referer SAVEQUERIES定数でDBクエリ記録
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の上位はbingbotSogou 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は役割が違います。前者は計測条件の識別で、後者はファイル名の区別です。この違いを説明するドキュメントが必要です。

  1. Lighthouseはブラウザから見たパフォーマンスを計測するツールで、First Contentful Paint (FCP)やLargest Contentful Paint (LCP)などのCore Web Vitalsを測定できます。しかし、サーバー側の処理内訳は取得できません。 – PageSpeed Insights
  2. WordPressのフック実行順序の詳細については、公式ドキュメントを参照してください。フロントエンド表示では、plugins_loaded → setup_theme → after_setup_theme → init → wp_loaded → wp → template_redirect → wp_head → wp_footer → shutdownの順で実行されます。 – Action Reference – WordPress Developer Resources
  3. より正確なパフォーマンス計測が必要な場合、PHP 7.3以降ではhrtime()関数の使用が推奨されています。ただし、WordPressの幅広い環境互換性を考慮するとmicrotime(true)で十分です。また、リクエスト全体の処理時間を測る場合は$_SERVER['REQUEST_TIME_FLOAT']を開始時刻として使用するとより正確です。 – PHP: microtime – Manual
  4. SAVEQUERIESを有効にするには、wp-config.phpファイルにdefine('SAVEQUERIES', true);を追記します。ただし、この機能はメモリ使用量とCPU負荷を増加させるため、本番環境では必ず無効化し、開発環境またはステージング環境でのみ使用してください。 – Save Database Queries for Analysis in WordPress
  5. JSON Lines (JSONL)は、1行に1つの完全なJSONオブジェクトを記録する形式で、ログファイルやストリーミングデータに適しています。ファイル拡張子は.jsonlが推奨されます。各行は独立したJSONオブジェクトであり、ファイル全体をJSON配列として解析する必要がないため、大量のログを効率的に処理できます。 – JSON Lines
  6. wp-content/uploads/配下のファイルは通常、Webブラウザから直接アクセスできます。ログに機密情報が含まれる可能性がある場合は、.htaccessでアクセス制限をかけるか、wp-content直下など別の場所に保存することを検討してください。 – WordPress Security Best Practices
  7. ログファイルは継続的に追記されるため、放置すると肥大化します。本番運用では定期的なクリアやログローテーションの仕組みが必要です。Linuxのlogrotateのような機能を実装するか、一定サイズを超えたら古いログをアーカイブする仕組みを検討してください。
  8. WordPressには既にQuery Monitorという優れたパフォーマンス分析プラグインが存在します。しかし、Query Monitorはリクエストごとの分析に特化しており、継続的な自動計測やON/OFF比較のための条件識別機能は提供していません。この記事のプラグインは、長期的なパフォーマンストレンド分析と比較実験に重点を置いています。 – Query Monitor Plugin
  9. the_contentフィルタは、投稿本文が表示される直前に実行されます。フィルタの第2引数に優先度(priority)を指定でき、数値が小さいほど早く実行されます。デフォルトは10です。複数のフィルタが登録されている場合、優先度の順に実行されます。 – Plugin API/Filter Reference – WordPress Codex
  10. WordPressのパフォーマンス最適化では、N+1クエリ問題(ループ内でのデータベースクエリ実行)が最大のボトルネックになることが多いです。メタデータの取得はupdate_postmeta_cache()で事前にキャッシュする、トランジェントAPIでクエリ結果をキャッシュするなどの対策が有効です。 – WordPress Performance Optimization
  11. クローラーは検索エンジンがウェブページを巡回して情報を収集する自動プログラムです。Google、Bing、Baiduなど各検索エンジンが独自のクローラーを運用しています。robots.txtファイルやmetaタグでクローラーの動作を制御できます。 – Robots.txt Specifications
  12. ログファイルへの同時書き込みは、PHPのfile_put_contents()にFILE_APPEND | LOCK_EXフラグを指定することで排他制御できます。LOCK_EXは書き込み時にファイルロックを取得し、他のプロセスが同時に書き込むのを防ぎます。 – PHP: file_put_contents – Manual
  13. WordPressのパフォーマンス計測では、オブジェクトキャッシュ(Redis、Memcached)、OPcache(PHPバイトコードキャッシュ)、ページキャッシュ(WP Rocket、LiteSpeed Cache)など複数のキャッシュ層が影響します。公平な比較のためには、各層のキャッシュ状態を揃える必要があります。 – WordPress Caching Best Practices