2026年9月2日

2026年9月2日

WordPressのページ速度を毎日監視するスクリプトの作り方(PageSpeed Insights API+cron)

WordPressサイトのページ速度を毎日記録したいなら、PageSpeed Insights API を呼び出す小さなスクリプトをサーバーの cron で1日1回動かし、結果をCSVに追記していく方法が手軽で長続きします。この記事では、API キーの準備、Python だけで動く監視スクリプト、cron への登録、しきい値を超えたときの通知、記録の読み方までを順に説明します。プラグイン更新やテーマ変更のあとに「いつから遅くなったのか」を日付で追えるようになるのが目標です。

毎日監視する意味と、測れるもの

PageSpeed Insights(以下PSI)をブラウザで開いて計測するのは、改善作業の直後には便利ですが、毎日手作業で続けるのは現実的ではありません。速度の悪化は、プラグインの自動更新、広告タグの追加、画像サイズの大きい記事の公開など、気づきにくい変化から始まることが多く、記録が残っていないと原因の切り分けに時間がかかります。

PSI の API が返す値は、大きく2種類あります。

  • ラボデータ(Lighthouse): Google のサーバーから1回だけ読み込んで測った値。パフォーマンススコア、LCP、TBT、CLS などが含まれます。計測のたびに多少ぶれます。
  • フィールドデータ(Chrome UX Report): 実際の Chrome 利用者から集めた過去28日間の値。LCP、INP、CLS の75パーセンタイルが返ります。アクセスが少ないページやサイトではデータが返らないことがあります。

Core Web Vitals の「良好」の目安は、Google の公式説明で LCP 2.5秒以下、INP 200ミリ秒以下、CLS 0.1以下(いずれも75パーセンタイル)とされています。毎日の監視では、ぶれの大きいラボ値で異変の兆しをつかみ、フィールドデータで実際の利用者への影響を確かめる、という役割分担で見るのが実用的です。

準備:API キーの取得と制限

PSI の API はキーなしでも呼べますが、キーなしでは利用回数の制限が厳しく、エラーで止まりやすくなります。毎日動かすならキーを用意してください。

  1. Google Cloud コンソールでプロジェクトを作成(既存のものでも可)
  2. 「API とサービス」から PageSpeed Insights API を有効化
  3. 認証情報で API キーを作成
  4. 作成したキーに「API の制限」を設定し、PageSpeed Insights API だけに使えるようにする

キーの制限は省略しがちですが、キーが漏れたときに他の API で悪用されるのを防ぐために設定しておきます。1日あたりの利用上限はコンソールの割り当て画面で確認できます。監視するURLが数件〜数十件であれば、1日1回の計測で上限に届くことはまずありません。

キーはスクリプトに直接書かず、環境変数で渡します。スクリプトをバックアップや Git に入れたときにキーが一緒に流出するのを避けるためです。

監視スクリプト(Python 標準ライブラリのみ)

サーバーに Python 3 が入っていれば、追加パッケージなしで動く形にしています。WordPress のプラグインとしてではなく、サーバー上の独立したスクリプトとして置くのがポイントです(理由は後述します)。

#!/usr/bin/env python3
# PageSpeed Insights API で指定URLを計測し、結果を CSV に追記する。
# しきい値を下回った(超えた)ときだけ標準出力に警告を出す。
import csv
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime

API = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
API_KEY = os.environ.get("PSI_API_KEY", "")
URLS = [
    "https://example.com/",
    "https://example.com/sample-post/",
]
STRATEGY = "mobile"  # デスクトップも測る場合は "desktop" で別に実行する
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CSV_PATH = os.path.join(BASE_DIR, "psi_log_%s.csv" % STRATEGY)

# 警告のしきい値(ラボ値)
MIN_SCORE = 50       # パフォーマンススコアがこれ未満なら警告
MAX_LAB_LCP = 4000   # ラボの LCP(ミリ秒)がこれを超えたら警告

FIELDS = ["date", "url", "score", "lab_lcp_ms", "lab_tbt_ms", "lab_cls",
          "field_lcp_ms", "field_inp_ms", "field_cls", "error"]


def call_psi(url):
    params = {"url": url, "strategy": STRATEGY, "category": "performance"}
    if API_KEY:
        params["key"] = API_KEY
    req = urllib.request.Request(API + "?" + urllib.parse.urlencode(params),
                                 headers={"User-Agent": "psi-monitor"})
    with urllib.request.urlopen(req, timeout=120) as res:
        return json.load(res)


def measure(url):
    # 一時的なエラーに備えて1回だけ再試行する
    for attempt in range(2):
        try:
            return call_psi(url)
        except (urllib.error.URLError, TimeoutError) as e:
            if attempt == 1:
                raise
            time.sleep(30)


def pick(data):
    lh = data.get("lighthouseResult", {})
    audits = lh.get("audits", {})
    field = data.get("loadingExperience", {}).get("metrics", {})
    score = lh.get("categories", {}).get("performance", {}).get("score")
    field_cls = field.get("CUMULATIVE_LAYOUT_SHIFT_SCORE", {}).get("percentile")
    return {
        "score": round(score * 100) if score is not None else "",
        "lab_lcp_ms": round(audits.get("largest-contentful-paint", {}).get("numericValue", 0)),
        "lab_tbt_ms": round(audits.get("total-blocking-time", {}).get("numericValue", 0)),
        "lab_cls": round(audits.get("cumulative-layout-shift", {}).get("numericValue", 0), 3),
        "field_lcp_ms": field.get("LARGEST_CONTENTFUL_PAINT_MS", {}).get("percentile", ""),
        "field_inp_ms": field.get("INTERACTION_TO_NEXT_PAINT", {}).get("percentile", ""),
        # フィールドの CLS は100倍された整数で返るため元の値に戻す
        "field_cls": field_cls / 100 if field_cls is not None else "",
    }


def main():
    today = datetime.now().strftime("%Y-%m-%d")
    is_new = not os.path.exists(CSV_PATH)
    with open(CSV_PATH, "a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        if is_new:
            writer.writeheader()
        for url in URLS:
            row = {"date": today, "url": url, "error": ""}
            try:
                row.update(pick(measure(url)))
            except Exception as e:
                row["error"] = str(e)[:200]
                print("[ERROR] %s: %s" % (url, row["error"]))
            writer.writerow(row)
            if row.get("score") != "" and row.get("score") is not None and row["score"] < MIN_SCORE:
                print("[WARN] %s スコア %s(しきい値 %s)" % (url, row["score"], MIN_SCORE))
            if row.get("lab_lcp_ms") and row["lab_lcp_ms"] > MAX_LAB_LCP:
                print("[WARN] %s ラボLCP %sms(しきい値 %sms)" % (url, row["lab_lcp_ms"], MAX_LAB_LCP))
            time.sleep(5)  # 連続リクエストを避ける


if __name__ == "__main__":
    main()

URLS には、トップページだけでなく、よく読まれる記事、カテゴリーアーカイブ、WooCommerce を使っているなら商品ページなど、テンプレートの異なるページを1つずつ入れておくと、どのテンプレートで問題が起きたかを切り分けやすくなります。全記事を並べる必要はありません。

まず手動で動かして、CSV ができるか確かめます。

mkdir -p ~/psi-monitor
cd ~/psi-monitor
PSI_API_KEY=your-api-key python3 psi_check.py
cat psi_log_mobile.csv

1URLあたり十数秒〜数十秒かかるのは正常です。error 列に HTTP 400 系のエラーが出る場合は、URL の誤り、ページが noindex やログイン必須で取得できない、キーの制限で API が許可されていない、といった原因が考えられます。

WP-Cron ではなくサーバーの cron で動かす理由

WordPress の中で動かしたくなるかもしれませんが、WP-Cron はサイトへのアクセスをきっかけに実行される仕組みです。アクセスが少ない時間帯には予定どおり動かず、逆に PSI のように1回の処理に時間がかかる作業を載せると、訪問者のリクエストと同じ PHP の実行時間制限に引っかかりやすくなります。また、計測対象のサイト自身が重くなっているときに監視処理まで同じサーバーの PHP に載せるのは、監視としても望ましくありません。

サーバーの cron(crontab)から独立したスクリプトとして動かせば、WordPress の状態に左右されず、決まった時刻に実行されます。レンタルサーバーでも、管理画面から cron を登録できるところが多くあります。

cron への登録と通知

crontab -e で次のように登録します。パスは環境に合わせて変えてください。

PSI_API_KEY=your-api-key
MAILTO=you@example.com
# 毎朝6時15分に計測(モバイル)
15 6 * * * /usr/bin/python3 /home/youruser/psi-monitor/psi_check.py

cron は、実行したコマンドが標準出力に何かを出したときだけ、その内容を MAILTO 宛てにメールで送ります。このスクリプトは警告やエラーがあるときだけ出力するので、「問題がない日はメールが来ない」という静かな監視になります。ただし、サーバーにメール送信の仕組みがない環境ではメールは届きません。その場合は、警告部分をチャットツールの Webhook に送る処理に置き換えるか、レンタルサーバーの cron 設定画面にある通知機能を使ってください。

python3 のパスはサーバーによって異なるため、which python3 で確かめてから書きます。cron はログインシェルの設定を読み込まないので、相対パスやシェルで設定した環境変数に頼ると動きません。キーを crontab に書く場合、crontab は自分のユーザーからしか読めないため、公開ディレクトリに置くよりは安全です。

記録の読み方とつまずきやすい点

ラボ値は、同じページでも計測ごとにスコアが数点〜10点程度ぶれることがあります。1日だけ下がったときに慌てて設定を変えるのではなく、数日続けて下がっているかを見てください。ぶれが気になる場合は、1URLを2〜3回計測して中央値を記録する形に改造する方法もあります。

確認の手順としては、次の順番が効率的です。

  1. CSV を表計算ソフトで開き、URL ごとにスコアとラボ LCP を折れ線グラフにする
  2. 悪化が始まった日付を特定する
  3. その日付の前後に行ったプラグイン更新、テーマ変更、記事公開、広告やタグの追加を、更新履歴やバックアップの記録と照らし合わせる
  4. 怪しい変更をステージング環境で戻し、PSI で差を確かめる

フィールドデータは過去28日間の集計なので、改善しても数値に反映されるまで日数がかかります。改善作業の効果をすぐに見たいときはラボ値、利用者にとって本当に良くなったかを判断するときはフィールドデータ、と使い分けます。Search Console の Core Web Vitals レポートも同じ Chrome UX Report を元にしているため、URL のグループ単位で傾向を見る補助として併用できます。

なお、PSI の API の仕様や返す項目は変わることがあります(2026年10月現在の仕様で説明しています)。キー名が見つからない場合に備えて、スクリプトでは .get() で空欄を許容する書き方にしてあります。列が空欄続きになったら、公式ドキュメントでレスポンスの構造を確認してください。

まとめ

  • PSI の API を1日1回呼び、ラボ値とフィールドデータを CSV に残すと、速度悪化の始まりを日付で追える
  • API キーは PageSpeed Insights API だけに制限し、スクリプトには直接書かず環境変数で渡す
  • 実行は WP-Cron ではなくサーバーの cron に任せ、警告があるときだけ出力して通知する
  • ラボ値は1日のぶれで判断せず数日の傾向で見る。フィールドデータは28日集計なので反映に時間がかかる
  • 悪化に気づいたら、その日付前後の変更履歴と照らし合わせ、ステージングで確かめてから本番を直す

お気軽にご相談ください

お見積りへ お問い合わせへ
▲