2026年9月2日

2026年9月2日

WordPressでCore Web Vitalsをリアルタイムに収集するRUM(実ユーザー計測)の設定

PageSpeed Insights の実データ(CrUX)は過去28日間の集計なので、改善した結果が数字に表れるまで時間がかかり、どのページで悪いのかも細かくはわかりません。実際の訪問者のブラウザで LCP・INP・CLS を計測して自分のサーバーに送る RUM(Real User Monitoring)を用意すると、変更の影響をその日のうちに確かめられます。この記事では、Google が公開している web-vitals ライブラリと WordPress の REST API を使った小さな RUM の作り方と、集計・運用の注意点を説明します。

RUMとCrUX・ラボデータの違い

Web Vitals の数値には、性質の違う3種類があります。

  • ラボデータ:Lighthouse などが、決まった条件で1回読み込んで測った値。再現性は高いものの、実際の利用者の体験とは一致しません
  • CrUX:Chrome の利用者から集めた実データ。Search Console のウェブに関する主な指標のレポートや PageSpeed Insights に使われます。28日間のまとまった集計で、アクセスが少ないページはデータが出ません
  • RUM:自分のサイトに組み込んだスクリプトで、訪問者ごとの値を自分で集めるもの。ページ単位・デバイス単位・時間単位で、好きな粒度で集計できます

RUMは CrUX の代わりではなく、補うものです。Googleが検索で参照するのは CrUX 側のデータなので、RUMは「どこが悪いか」「直したら良くなったか」を早く知るための道具と位置づけます。

指標の目安は、LCP が2.5秒以内、INP が200ミリ秒以内、CLS が0.1以下で「良好」とされ、いずれも訪問の75パーセンタイル値で評価します。INP は2024年3月に FID に代わって Core Web Vitals の指標になりました。RUM でも平均値ではなく75パーセンタイル値で見ると、CrUX と比べやすくなります。

全体の構成

今回作る仕組みは次のとおりです。

  1. web-vitals ライブラリが、訪問者のブラウザで各指標の最終値を計測する
  2. 計測値を navigator.sendBeacon() で WordPress の REST API エンドポイントに送る
  3. WordPress 側で値を検証し、専用テーブルに保存する
  4. SQL で直近の75パーセンタイル値をページ別・デバイス別に集計する

訪問ごとに書き込みが発生するため、アクセスの多いサイトでは送信をサンプリング(例:10%の訪問だけ送る)して、データベースの負荷を抑えます。作業前にサイトとデータベースのバックアップを取り、ステージング環境で動作を確かめてから本番に入れてください。

ステップ1:web-vitalsライブラリを用意する

web-vitals は Google Chrome チームが公開しているオープンソースのライブラリで、onLCP・onINP・onCLS・onFCP・onTTFB といった関数で各指標を受け取れます。主要バージョンは v5 系で(2026年10月現在)、v5 では廃止された FID 用の関数が削除されています。

npm パッケージの dist フォルダには、<script> タグで読み込める IIFE 版(web-vitals.iife.js)が含まれており、読み込むと webVitals というグローバル変数から関数を呼び出せます。外部のCDNに依存しないよう、このファイルをプラグインの js フォルダに置いて自サイトから配信します。ファイル名や構成はバージョンで変わることがあるため、取得時にパッケージの README を確認してください。

ステップ2:受信用のプラグインを作る

wp-content/plugins/simple-rum/simple-rum.php を作成し、次のコードを書きます。有効化したときに専用テーブルを作り、REST API で計測値を受け取ります。

<?php
/**
 * Plugin Name: Simple RUM for Web Vitals
 * Description: web-vitals で計測した値を REST API で受け取り、専用テーブルに保存する。
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

function simple_rum_table() {
	global $wpdb;
	return $wpdb->prefix . 'rum_vitals';
}

// 有効化時にテーブルを作成する
register_activation_hook( __FILE__, function () {
	global $wpdb;
	require_once ABSPATH . 'wp-admin/includes/upgrade.php';
	$table   = simple_rum_table();
	$charset = $wpdb->get_charset_collate();
	dbDelta( "CREATE TABLE {$table} (
		id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
		metric varchar(8) NOT NULL,
		value double NOT NULL,
		rating varchar(20) NOT NULL DEFAULT '',
		path varchar(255) NOT NULL DEFAULT '',
		device varchar(10) NOT NULL DEFAULT '',
		created_at datetime NOT NULL,
		PRIMARY KEY  (id),
		KEY metric_created (metric, created_at)
	) {$charset};" );
} );

// 計測スクリプトを読み込む(ログイン中のユーザーは計測しない)
add_action( 'wp_enqueue_scripts', function () {
	if ( is_user_logged_in() ) {
		return;
	}
	// バージョン文字列は、実際に配置した web-vitals のバージョンに合わせる
	wp_enqueue_script( 'web-vitals', plugins_url( 'js/web-vitals.iife.js', __FILE__ ), array(), '5', array( 'in_footer' => true ) );
	wp_enqueue_script( 'simple-rum', plugins_url( 'js/rum.js', __FILE__ ), array( 'web-vitals' ), '1.0.0', array( 'in_footer' => true ) );

	$config = array(
		'endpoint'   => esc_url_raw( rest_url( 'simple-rum/v1/vitals' ) ),
		'sampleRate' => 0.1, // 10% の訪問だけ送信する
	);
	wp_add_inline_script( 'simple-rum', 'window.simpleRum = ' . wp_json_encode( $config ) . ';', 'before' );
} );

// 計測値を受け取るエンドポイント
add_action( 'rest_api_init', function () {
	register_rest_route( 'simple-rum/v1', '/vitals', array(
		'methods'             => 'POST',
		// 匿名の訪問者から受け取るため公開する。その分、値は args で厳しく検証する
		'permission_callback' => '__return_true',
		'callback'            => 'simple_rum_store',
		'args'                => array(
			'name'   => array(
				'required'          => true,
				'type'              => 'string',
				'enum'              => array( 'LCP', 'INP', 'CLS', 'FCP', 'TTFB' ),
				'validate_callback' => 'rest_validate_request_arg',
			),
			'value'  => array(
				'required'          => true,
				'type'              => 'number',
				'minimum'           => 0,
				'maximum'           => 600000,
				'validate_callback' => 'rest_validate_request_arg',
			),
			'rating' => array(
				'type'              => 'string',
				'enum'              => array( 'good', 'needs-improvement', 'poor' ),
				'validate_callback' => 'rest_validate_request_arg',
			),
			'device' => array(
				'type'              => 'string',
				'enum'              => array( 'mobile', 'desktop' ),
				'validate_callback' => 'rest_validate_request_arg',
			),
			'path'   => array(
				'type'              => 'string',
				'maxLength'         => 2048,
				'validate_callback' => 'rest_validate_request_arg',
				'sanitize_callback' => 'sanitize_text_field',
			),
		),
	) );
} );

function simple_rum_store( WP_REST_Request $request ) {
	global $wpdb;

	// クエリ文字列は保存しない(個人情報が含まれる可能性があるため)
	$path = (string) wp_parse_url( (string) $request->get_param( 'path' ), PHP_URL_PATH );

	$wpdb->insert(
		simple_rum_table(),
		array(
			'metric'     => $request->get_param( 'name' ),
			'value'      => (float) $request->get_param( 'value' ),
			'rating'     => (string) $request->get_param( 'rating' ),
			'path'       => substr( $path, 0, 255 ),
			'device'     => (string) $request->get_param( 'device' ),
			'created_at' => current_time( 'mysql', true ), // UTC で保存
		),
		array( '%s', '%f', '%s', '%s', '%s', '%s' )
	);

	return new WP_REST_Response( null, 204 );
}

エンドポイントは誰でも呼び出せるため、指標名・評価・デバイスは決まった値だけを受け付け、数値には上限を設けています。IPアドレスやCookieは保存していません。不正な値を大量に送られる可能性はゼロではないので、気になる場合はWAFやサーバー側のレート制限も併用してください。

ステップ3:ブラウザ側の送信スクリプト

同じプラグインの js/rum.js に次を書きます。web-vitals のコールバックは、初期設定では各指標の最終値が決まった時点(ページが非表示になるときなど)で1回呼ばれます。

(function () {
  var cfg = window.simpleRum;
  if (!cfg || !window.webVitals || Math.random() > cfg.sampleRate) {
    return;
  }
  var device = window.matchMedia('(max-width: 768px)').matches ? 'mobile' : 'desktop';

  function send(metric) {
    var body = JSON.stringify({
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      path: location.pathname,
      device: device
    });
    var blob = new Blob([body], { type: 'application/json' });
    if (!(navigator.sendBeacon && navigator.sendBeacon(cfg.endpoint, blob))) {
      fetch(cfg.endpoint, {
        method: 'POST',
        body: body,
        headers: { 'Content-Type': 'application/json' },
        keepalive: true
      });
    }
  }

  webVitals.onLCP(send);
  webVitals.onINP(send);
  webVitals.onCLS(send);
  webVitals.onFCP(send);
  webVitals.onTTFB(send);
})();

sendBeacon() はページを閉じる直前でも送信が途切れにくいため、RUMの送信に向いています。デバイスの判定は画面幅による簡易的なものなので、厳密に分けたい場合は判定方法を見直してください。

プラグインを有効化したら、ログアウトした状態(またはシークレットウィンドウ)でページを開き、開発者ツールの Network パネルで vitals へのリクエストが 204 で返ることを確認します。動作確認のあいだだけ sampleRate を 1 にしておくと確認しやすくなります。ブラウザによって対応している指標が異なり、対応していない指標は送られません。

ステップ4:直近の値を集計する

データがたまったら、直近24時間の75パーセンタイル値を指標別・デバイス別に出します。MySQL 8.0 以降や MariaDB 10.2 以降で使えるウィンドウ関数を利用した例です。テーブル名の wp_ は、自分のサイトの接頭辞に合わせてください。

wp db query "SELECT metric, device, MIN(value) AS p75 FROM (SELECT metric, device, value, PERCENT_RANK() OVER (PARTITION BY metric, device ORDER BY value) AS pr FROM wp_rum_vitals WHERE created_at >= UTC_TIMESTAMP() - INTERVAL 1 DAY) AS t WHERE pr >= 0.75 GROUP BY metric, device;"

PARTITION BY に path を加えればページ別の値になります。件数が少ないページの75パーセンタイル値はぶれやすいので、サンプル数もあわせて確認しながら判断します。修正を公開した直後から数時間おきに集計すれば、改善が表れたかを CrUX より早く確かめられます。

古いデータは定期的に削除します。削除の前にはテーブルのバックアップを取ってください。

wp db query "DELETE FROM wp_rum_vitals WHERE created_at < UTC_TIMESTAMP() - INTERVAL 30 DAY;"

自前で保存しない選択肢

データベースに書き込みたくない場合は、送信先を Google アナリティクス 4(GA4)にする方法もあります。send 関数の中で gtag('event', metric.name, { value: metric.value, metric_id: metric.id }) のようにイベントとして送れば、GA4 のリアルタイムレポートで受信を確認でき、探索レポートでページ別に分析できます(レポート名や画面構成は変わることがあります)。また、CDNや監視サービスの中には、RUM機能を提供しているものもあります。

いずれの方法でも、訪問者のブラウザから計測データを送ることになります。プライバシーポリシーへの記載を見直し、外部サービスに送る場合は、事業の形態に応じて電気通信事業法の外部送信規律への対応が必要かどうかも確認してください。

まとめ

  • RUMは訪問者の実際の体験を自分で集める仕組みで、28日集計の CrUX を補い、改善の効果を早く確かめるために使う
  • web-vitals ライブラリで計測し、sendBeacon() で WordPress の REST API に送れば、小さなプラグインで RUM を作れる
  • 公開エンドポイントは受け付ける値を厳しく検証し、送信はサンプリングして負荷を抑える
  • 集計は平均ではなく75パーセンタイル値で行い、デバイス別・ページ別に見る
  • 個人情報を保存しない設計にし、プライバシーポリシーも見直す

お気軽にご相談ください

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