Statnive を低オーバーヘッドに設計した方法
非同期読み込み、インラインコアトラッカー、アイドルコールバックという 3 つのアーキテクチャ変更で、Statnive のベンチマーク LCP 影響を半分に削減しました。エンジニアリングのストーリーと、正直な注意点をお伝えします。
最下位から、テストで最も低いオーバーヘッドへ
最初に Statnive を他の 7 つの WordPress アナリティクスプラグインと比較ベンチマークしたとき、結果は手厳しいものでした。TTFB は優秀で、4 番目に高速でした。しかし Largest Contentful Paint は、セルフホスト型プラグインの中で最下位でした。サーバーレスポンスから訪問者が実際にコンテンツを目にするまでの差は 202 ミリ秒。Koko Analytics は 94ms、Burst Statistics は 80ms、私たちは 202ms でした。
問題はトラッカーコード自体ではなく、WordPress がそれを読み込む方法でした。
その日の終わりまでに、私たちはこの差を 79 ミリ秒まで縮めました。LCP は 504ms から 288ms へ、43% の改善です。軽負荷では同率 2 位までたどり着きました。続く合成ストレステスト (50 並列 HTTP ユーザー、ページキャッシュなし) では、Statnive は計測した 8 プラグインの中で最も低い LCP オーバーヘッドを示しました。私たちが何を変えたか、なぜ変えたか、そしてベンチマーク数値が示すこと・示さないことについての正直な注意点を以下にまとめます。
ベンチマーク: 8 プラグイン、本物の Chromium、本物の負荷
私たちは、WordPress REST API 経由でアナリティクスプラグインを切り替え、k6 経由で本物の Chromium ブラウザ訪問を実行し、PerformanceObserver で Core Web Vitals を収集する自動テストフレームワークを構築しました。各プラグインは完全に分離されて実行され、他のすべてのアナリティクスプラグインは無効化、キャッシュはフラッシュされ、計測開始前に 5 リクエストでサーバーをウォームアップしています。
ビフォーアフターの結果:
| 指標 | 改善前 | 改善後 | 変化 |
|---|---|---|---|
| Statnive TTFB | 294ms | 209ms | -29% |
| Statnive FCP | 496ms | 288ms | -42% |
| Statnive LCP | 504ms | 288ms | -43% |
| TTFB から LCP までの差 | 202ms | 79ms | -61% |
| 順位 (LCP) | 8 件中 7 位 | 2 位 (同率) | +5 ポジション |
根本原因: 3 つのパフォーマンスキラー
私たちは 202ms の差を FrontendHandler.php 内の 3 つの問題まで遡って特定し、それぞれ WordPress Core のドキュメントとウェブパフォーマンス研究で独立に確認しました。
問題 1: wp_localize_script がブロッキングモードを強制する。 WordPress 6.3 は strategy パラメータを通じて async/defer のネイティブサポートを導入しました。しかし wp_localize_script() は「after」位置にインラインスクリプトを生成しますが、これは WordPress Core Trac #58632 の通り、親スクリプトをブロッキングモードに強制し、依存関係ツリー全体に伝播します。チェーン中のすべてのスクリプトが async/defer 戦略を失います。
問題 2: async または defer 属性がない。 私たちのトラッカーは ['in_footer' => true] で登録されていましたが、strategy パラメータがありませんでした。フッターであっても、同期スクリプトはダウンロードと実行が完了するまでブラウザの load イベント発火をブロックします。
問題 3: SRI ハッシュが毎ページロードで計算される。 私たちはすべてのページリクエストで file_get_contents() + hash('sha256', ...) を呼び出して Subresource Integrity ハッシュを生成していました。これは訪問者ごとのファイルシステム読み取り + CPU 集約的なハッシングです。
Statnive を入手: パフォーマンス重視のセルフホスト型アナリティクス
ここで述べたすべての最適化は、現在の Statnive に同梱されています。WordPress.org から無料でインストール — データはサーバーに留まり、ページは速いままです。
フェーズ 1: 読み込み戦略を修正する
最大の勝因は、FrontendHandler.php への 3 つの変更でした。
wp_localize_script を wp_add_inline_script('before') に置き換える。 'before' 位置が決定的です。これはブロッキングモードに伝播しません。'after' 位置 (これがデフォルト) は伝播します。この区別は公式の WordPress 6.3 スクリプト読み込み発表で文書化されていますが、見落としやすいものです。
// Before (forces blocking):
wp_localize_script( 'statnive-tracker', 'StatniveConfig', $config );
// After (safe with async):
wp_add_inline_script(
'statnive-tracker',
'window.StatniveConfig=' . wp_json_encode( $config ) . ';',
'before' // MUST be 'before' — 'after' cascades to blocking
);
wp_enqueue_script に strategy: 'async' を追加する。 DOM アクセスを必要としないアナリティクストラッカーには、async の方が defer よりも適しています。Defer は HTML の完全パース (複雑なページでは 500ms 以上) を待ちます。Async はダウンロード完了次第に実行されます。私たちのトラッカーは window.StatniveConfig を読み、navigator.sendBeacon() を発火するだけで、どちらも DOM を必要としません。
SRI ハッシュを WordPress トランジェントにキャッシュする。 filemtime() をキーとして、ハッシュは一度だけ計算され、ファイルが変わるまで再利用されます。新しいビルド = 新しい更新時刻 = 自動キャッシュ無効化です。
フェーズ 2: メインスレッドを解放する
非同期読み込みが整った後、私たちはトラッカー JavaScript 自体に取り掛かりました。
DOMContentLoaded ラッパーを取り除く。 async ではスクリプトはダウンロード次第に実行されます。トラッカーは window と navigator のグローバルを読むだけで、DOM は不要です。DOMContentLoaded イベントリスナは不要な遅延を加えていました。
requestIdleCallback 経由で重要でないモジュールを遅延する。 ページビューヒットが唯一のクリティカルパス操作です。エンゲージメント追跡 (スクロール深度、ページ滞在時間)、自動追跡 (外部リンク、フォーム送信)、CSS イベント追跡は、すべてブラウザがアイドルになるまで待てます。Safari は 2024 年 9 月以降 requestIdleCallback をネイティブサポートしているため、最新ブラウザではポリフィルは不要です。
// Critical path: fires immediately
sendHit(buildPayload());
// Deferred: runs when browser is idle
var idle = window.requestIdleCallback || function(cb) { setTimeout(cb, 80); };
idle(function() {
engagementTracker.start();
registerAutoTracking(sendEvent);
});
私たちの調査からの重要な洞察: requestIdleCallback に timeout パラメータを渡してはいけません。タイムアウトはユーザーの操作中であっても実行を強制し、ジャンクや INP スコアの悪化を引き起こす可能性があります。本当にアイドルかどうかは、ブラウザに判断させましょう。
フェーズ 3: 外部リクエストを排除する
最後の最適化では、クリティカルレンダリングパスから外部スクリプトのダウンロードを完全に排除します。Google の gtag.js がキューベースのインラインブートストラップを使う方法、そして Koko Analytics が 468 バイトのトラッカー全体をインライン化する方法から着想を得て、2 段階アーキテクチャを構築しました。
ステージ 1: インラインコアトラッカー (1.1KB)。 設定を読み、プライバシーシグナル (DNT/GPC) をチェックし、4 つのボット検出ヒューリスティックを実行し、ページビューペイロードを構築し、navigator.sendBeacon() で発火する最小限の IIFE です。wp_footer 内の wp_print_inline_script_tag() を介して HTML に直接出力されます。外部リクエストはゼロです。
ステージ 2: 非同期フルトラッカー (5KB)。 エンゲージメント、イベント、自動追跡、同意管理を備えた完全なトラッカーが strategy: 'async' で読み込まれます。初期化時に window.statnive_hit_sent をチェックし、インラインコアが既にページビューを発火していれば、遅延モジュールの初期化に直接進みます。重複ヒットはありません。
結果として、ページビューはどの外部リソースの読み込みが完了するよりも前に、インライン JavaScript から発火します。完全な機能セットはバックグラウンドで読み込まれ、Core Web Vital に影響しません。
フェーズ別の結果
各フェーズは独立して展開・計測されました。
| フェーズ | 変更内容 | 差分 | LCP |
|---|---|---|---|
| 最適化前 | ブロッキングスクリプト、戦略なし | 202ms | 504ms |
| フェーズ 1: async + インライン設定 | 非ブロッキングダウンロード | 〜80ms | 〜374ms |
| フェーズ 2: requestIdleCallback | メインスレッド解放 | 〜65ms | 〜359ms |
| フェーズ 3: インラインコアトラッカー | 外部リクエストゼロ | 79ms | 288ms |
合成ストレステスト: アーキテクチャは負荷下でどう振る舞うか
軽負荷ではアーキテクチャ的な差が隠れる可能性があります。8 プラグインすべてをストレステストするため、10 名の Chromium ブラウザユーザーが Core Web Vitals を計測しつつ、50 名の HTTP ユーザーが並行してサーバーを叩く構成でベンチマークを再実行しました。ページキャッシュプラグインは未インストール。すべてのリクエストが完全な WordPress PHP パスにヒットしました。これは、競合下でどのプラグインが劣化するかを暴くために設計された病的な条件です。
結果 — 単一実行のストレステストでベースラインに対する LCP オーバーヘッド、プラグインあたり 〜150 サンプル:
| 順位 | プラグイン | LCP Δ | 影響スコア |
|---|---|---|---|
| 1 | Statnive | +260ms | 6.7 |
| 2 | Independent Analytics | +566ms | 14.2 |
| 3 | Jetpack | +776ms | 19.5 |
| 4 | MonsterInsights (GA4) | +964ms | 24.1 |
| 5 | WP Slimstat | +1030ms | 25.4 |
| 6 | WP Statistics | +1424ms | 35.9 |
| 7 | Koko Analytics | +2278ms | 56.3 |
| 8 | Burst Statistics | +3592ms | 89.6 |
これらは本番運用の数値ではありません。 開発マシン上、キャッシュなしでの単一実行の結果です。W3TC、WP Rocket、CDN ページキャッシュを備えた本番 WordPress サイトでは、キャッシュされたページがアナリティクスの PHP コードをまったく実行しないため、差は劇的に小さくなります。とくに Koko Analytics と Burst Statistics の大きな LCP 差は、おそらく実サイトでの定常状態のオーバーヘッドではなく、テスト固有の競合問題 (WP-Cron バッチ処理、データベース書き込みのシリアライズ) を反映しています。
このテストが示すのは、Statnive のアーキテクチャがサーバー側の競合に関係なくクリティカルレンダリングパスを清潔に保つということです。インラインコアはサーバー作業が始まる前に navigator.sendBeacon() を発火するため、データベースが高負荷でもページビューが捕捉されます。アーキテクチャ的な勝因こそがストーリーであり、特定の倍率ではありません。あなた自身の構成について結論を出す前に、自分のハードウェアでテストを実行してください。
研究に裏打ちされた決定
すべての技術的決定は、公開された研究と公式ドキュメントに照らして検証されました。WordPress Core Trac チケット、web.dev のパフォーマンスガイド、W3C 仕様、本番信頼性研究にわたる 100 件以上のソースを参照しました。アプローチを形作った主な発見は次のとおりです。
wp_add_inline_script('before')は、async/defer 戦略と併用しても安全であると明記されています (Make WordPress Core、2023 年 7 月)createElement経由のスクリプト注入は、ブラウザのプリロードスキャナをバイパスするため、ネイティブの<script async>より 2.1 秒遅くなります (Ilya Grigorik、Google)navigator.sendBeacon()はvisibilitychangeおよびpagehideイベントと組み合わせると 95.8〜98% の配信信頼性を達成します (NicJ.net 本番調査、200 万ページビュー以上)- モバイルの JavaScript パース・コンパイルはデスクトップより 2〜5 倍遅いですが、当社のトラッカーは 5KB で、分割が必要となる 50KB のしきい値を大きく下回っています (Addy Osmani、Google)
よくある質問
インラインコアトラッカーはコンテンツセキュリティポリシーで動作しますか
はい。wp_print_inline_script_tag() は WordPress の wp_inline_script_attributes フィルタを尊重し、CSP 準拠のために nonce を追加できます。インラインスクリプトはサーバー側で生成され、ユーザー入力を含みません。
非同期フルトラッカーが読み込みに失敗したらどうなりますか
ページビューはすでにインラインコアによって記録されています。そのセッションのエンゲージメントとイベント追跡は失われますが、コアアナリティクスデータは捕捉されます。これは緩やかな劣化です。最も重要な指標 (ページビュー) が最も信頼性の高い配信を持ちます。
フルトラッカーで defer ではなく async を使う理由は
Defer は完全な HTML パースを待ってから実行します。DOM を操作しないアナリティクストラッカーには、これは不要な遅延です。Async は並行してダウンロードし、即座に実行されます。インラインの 'before' スクリプトが、async スクリプト実行前に StatniveConfig が利用可能であることを保証します。
6.3 より前の WordPress バージョンでこのアプローチは機能しますか
strategy パラメータには WordPress 6.3 以降が必要です。古いバージョンではパラメータは黙って無視され、スクリプトは標準のフッタースクリプトとして読み込まれます。機能はしますが async 最適化はありません。Statnive は WordPress 6.4 以降を必要とします。
次にやること
私たちのトラッカーは合成ストレステストで 1 位を取りましたが、単一実行のベンチマークは本番実証と同じではありません。次に調査する領域:
- 分散レポート付きの複数実行ベンチマーク: ヘビーティアテストをランダム化された設定順で 5 回実行し、単一実行の中央値ではなく中央値と四分位範囲を報告する
- ページキャッシュ有効時のベンチマーク: W3TC や WP Rocket と組み合わせて全プラグインをテストし、現実的な本番セットアップでの比較を示す
- 独立検証: フレームワーク全体がオープンソースです。第三者にぜひ走らせていただき、独自の結果を公開していただきたいです
- コンパイル時の機能バリアント (Plausible のモデル): 有効化された機能に基づいて異なるトラッカービルドを生成し、エンゲージメント追跡を使わないサイトはさらに小さなスクリプトを得られるようにする
- Service Worker による永続化: 不安定なモバイル接続でも配信信頼性を保つために、Service Worker でイベントをキューに入れる
- サーバー側 TTFB の削減: PHP ヒットエンドポイントをプロファイルし、サーバーレスポンスからミリ秒を削る
パフォーマンスは 1 度出荷すれば終わりの機能ではありません。リリースのたびに鍛える規律であり、正直な計測はその規律の一部です。
Google Analytics、MonsterInsights、他の WordPress アナリティクスプラグインと Statnive のパフォーマンスを比較してご覧ください。あるいは、Statnive のすべての機能を探索してください。